Compare commits
75
Commits
param_foyer
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
536f479423 | ||
|
|
685c649e7c | ||
|
|
074d8e12d2 | ||
|
|
f748330a1b | ||
|
|
ebdbee71db | ||
|
|
659b286967 | ||
|
|
ac8ebf7328 | ||
|
|
49d4ec7c35 | ||
|
|
c48a6ba116 | ||
|
|
20d6c9b7b0 | ||
|
|
d856d0a409 | ||
|
|
71ec8bd68a | ||
|
|
86ef3ccbf2 | ||
|
|
e7aa3b595b | ||
|
|
3e98de9c18 | ||
|
|
6b2f83b68c | ||
|
|
7e24ca7612 | ||
|
|
506cd64a44 | ||
|
|
041411b070 | ||
|
|
fa6521f8f5 | ||
|
|
5618448602 | ||
|
|
9d22dc29b4 | ||
|
|
b3a4614c59 | ||
|
|
18740c5e1b | ||
|
|
854c6388ba | ||
|
|
1e090cf9ca | ||
|
|
ff83320f9a | ||
|
|
557a1451de | ||
|
|
03a97df8f1 | ||
|
|
ad5a845f18 | ||
|
|
142d4612a9 | ||
|
|
2e30569675 | ||
|
|
f5c5320189 | ||
|
|
5b4f4c68b2 | ||
|
|
aad528d15b | ||
|
|
89d8ad114b | ||
|
|
f36bfa828e | ||
|
|
f9aa368768 | ||
|
|
e73524b2ae | ||
|
|
96df90d372 | ||
|
|
d6e95c1b2a | ||
|
|
16b08dd03b | ||
|
|
6459c225ac | ||
|
|
429a4ccb66 | ||
|
|
cd79bbb71d | ||
|
|
ecac9479d5 | ||
|
|
429a964727 | ||
|
|
70462aa78b | ||
|
|
afa6587eb3 | ||
|
|
c26a369bb6 | ||
|
|
305b58653d | ||
|
|
8e3e7fe3d8 | ||
|
|
2c8bbbb3e1 | ||
|
|
0ff3c42d35 | ||
|
|
ea04831451 | ||
|
|
d9ae5308ea | ||
|
|
2c5fb9da6b | ||
|
|
751082308a | ||
|
|
b44688a654 | ||
|
|
f8e2ad67ee | ||
|
|
6aab112ecb | ||
|
|
6e409fb3a1 | ||
|
|
535f1b7dcf | ||
|
|
4130b03a68 | ||
|
|
315e1b5fb1 | ||
|
|
b1d3db4892 | ||
|
|
99ed0e31de | ||
|
|
a1e7bc62fd | ||
|
|
3675f0c01a | ||
|
|
836533fcf6 | ||
|
|
5fe9046a63 | ||
|
|
5b70cd20e6 | ||
|
|
ed2d5a7c9e | ||
|
|
6bbee271e3 | ||
|
|
7b294f300a |
@@ -2,7 +2,7 @@ name: Deploy HouseHub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [main, staging-perco, staging-fefe]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
@@ -15,6 +15,21 @@ jobs:
|
||||
username: perco
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
script: |
|
||||
BRANCH="${{ github.ref_name }}"
|
||||
case "$BRANCH" in
|
||||
main)
|
||||
cd /opt/container/househub
|
||||
git pull
|
||||
echo "✓ HouseHub mis à jour"
|
||||
echo "✓ Production mise à jour"
|
||||
;;
|
||||
staging-perco)
|
||||
cd /opt/container/househub-perco
|
||||
git pull origin staging-perco
|
||||
echo "✓ Staging Perco mis à jour"
|
||||
;;
|
||||
staging-fefe)
|
||||
cd /opt/container/househub-fefe
|
||||
git pull origin staging-fefe
|
||||
echo "✓ Staging Fefe mis à jour"
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
househub_db_data
|
||||
.env
|
||||
data/.hh_app_secret
|
||||
extract_source.php
|
||||
.htaccess
|
||||
@@ -151,12 +151,15 @@ L'architecture de HouseHub isole chaque foyer dans sa propre base de données (`
|
||||
Afin de transformer complètement l'application héritée de Pacha Family en un produit générique et autonome pour chaque espace familial, les chantiers suivants doivent être menés.
|
||||
|
||||
🟢 Niveau 1 : Facile (Constantes et configurations globales)
|
||||
|
||||
[x] Configuration de base : Extraire la devise (CURRENCY) et la zone de vacances scolaires (ZONE_SCOLAIRE) du fichier config.php pour les stocker dans les paramètres d'espace en BDD.
|
||||
[x] Suppression des IDs statiques : Supprimer les IDs parents fixes (ID_ALEX = 2, ID_LAIA = 3) dans config.php au profit d'une lecture dynamique basée sur les utilisateurs réels de la famille connectée (liens via la table pf_people).
|
||||
[x] Charte graphique dynamique : Remplacer les couleurs codées en dur (bleu/orange) par des sélecteurs natifs dans l'espace "Paramètres", en générant les variables CSS et leurs nuances (via color-mix()) à la volée.
|
||||
[ ] Filtres API Éducation Nationale : Rendre dynamique la zone de l'API (zones LIKE '%Zone C%' dans family-calendar.js) pour s'adapter à la région de chaque foyer.
|
||||
[ ] Identifiant Planka : Variabiliser le project_id Planka par défaut écrit en dur dans planka/api.php.
|
||||
[ ] Suppression des IDs statiques : Supprimer les IDs parents fixes (ID_ALEX = 2, ID_LAIA = 3) dans config.php au profit d'une lecture dynamique basée sur les utilisateurs réels de la famille connectée.
|
||||
|
||||
🟡 Niveau 2 : Intermédiaire (Listes et dictionnaires en dur)
|
||||
|
||||
[ ] Gestion des membres (Cadeaux) : Dans gift-list.php, remplacer la liste d'enfants ($children) et d'adultes ($baseAdults, $extraAdults) codée en dur par une lecture dynamique des membres du foyer déclarés en BDD.
|
||||
[ ] Occasions festives (Cadeaux) : Rendre configurables les occasions spéciales (TIO, NOEL, ROIS, ANNIV, SANT).
|
||||
[ ] Configuration des catégories (Budget) : Dans budget/views/suivi.php, migrer le tableau $categoriesConfig (FMCG, Essence, École, etc.), leurs couleurs et leurs suggestions de magasins associés vers un stockage paramétrable.
|
||||
@@ -164,7 +167,9 @@ Afin de transformer complètement l'application héritée de Pacha Family en un
|
||||
[ ] Types de carburants (Garage) : Variabiliser la liste statique des carburants dans garage.php.
|
||||
|
||||
🔴 Niveau 3 : Complexe (Logique métier et Schéma SQL)
|
||||
[ ] Refonte relationnelle de la répartition (Budget) : Modifier la structure de la table pf_alloc_values pour supprimer les colonnes figées amount_alex et amount_laia et basculer sur un modèle relationnel (alloc_id, user_id, amount) capable de gérer n'importe quel nombre d'adultes (1, 2, 3 ou plus).
|
||||
|
||||
[x] Refonte relationnelle du Budget (Prévisionnel) : Refonte totale de budget_prev.php. Modification de la table pf_alloc_values pour basculer sur des colonnes génériques (amount_p1, amount_p2) et suppression définitive de toute logique conditionnelle liée aux prénoms des concepteurs originaux.
|
||||
[ ] Refonte globale de la répartition (Budget) : Étendre le modèle relationnel (alloc_id, user_id, amount) au reste de l'application pour gérer n'importe quel nombre d'adultes (1, 2, 3 ou plus).
|
||||
[ ] Destinations de transferts (Budget) : Rendre dynamiques les cibles de virements prévisionnels (vers L.Pol, vers L.Pep, etc.) en fonction des comptes épargne créés par la famille.
|
||||
[ ] Onglets d'Épargne : Générer dynamiquement les sous-onglets de la vue epargne.php à partir des membres réels au lieu des blocs statiques Alex, Laia et Nens.
|
||||
[ ] Types de Garde (Calendrier) : Supprimer les types d'événements et compteurs codés en dur (OFF_CAROLE, EXTRA_OFF_CAROLE, CENTRE, AVIS, PEP_SICK) pour permettre à chaque foyer de définir ses propres modalités de garde et compteurs de congés associés.
|
||||
@@ -175,7 +180,8 @@ Afin de transformer complètement l'application héritée de Pacha Family en un
|
||||
## 📌 Versioning
|
||||
|
||||
| Version | Changements |
|
||||
| ------- | ------------------------------------------------------------------------------------------ |
|
||||
| ------- | ------------------------------------------------------------------------------------------------------------------------- |
|
||||
| v2.3.0 | Multi-tenant (p.2) : Refonte BDD Budget (p1, p2), suppression IDs statiques et thèmes de profils dynamiques (color-mix()) |
|
||||
| v2.2.0 | Multi-tenant (p.1) : Paramètres dynamiques du foyer (€, Zone) via BDD + script migration |
|
||||
| v2.1.0 | Module Planka : Kanban natif connecté à Planka via API proxy PHP |
|
||||
| v2.0.0 | Module PrintVault : viewer 3D STL/3MF/GCode, stockage local, trajectoires GCode illimitées |
|
||||
|
||||
+35
-27
@@ -4,6 +4,18 @@ require __DIR__ . '/includes/db.php';
|
||||
require_once __DIR__ . '/includes/i18n.php';
|
||||
require_login('/login.php');
|
||||
|
||||
// ============================================================================
|
||||
// DIAGNOSTIC ONBOARDING (Vérifie si le budget est prêt à être utilisé)
|
||||
// ============================================================================
|
||||
$currentYear = date('Y');
|
||||
$hasCategories = ((int)$pdo->query("SELECT COUNT(*) FROM pf_budget_categories")->fetchColumn() > 0);
|
||||
$hasSalaries = ((int)$pdo->query("SELECT COUNT(*) FROM pf_salary_config WHERE year = $currentYear")->fetchColumn() > 0);
|
||||
$hasAccounts = ((int)$pdo->query("SELECT COUNT(*) FROM pf_bank_accounts")->fetchColumn() > 0);
|
||||
$hasBudgetItems = ((int)$pdo->query("SELECT COUNT(*) FROM pf_budget_items")->fetchColumn() > 0);
|
||||
|
||||
// Le minimum vital pour que ça tourne
|
||||
$isBudgetSetupOk = ($hasCategories && $hasSalaries && $hasAccounts);
|
||||
|
||||
// Gestion de l'onglet actif (par défaut 'suivi')
|
||||
$tab = $_GET['tab'] ?? 'suivi';
|
||||
|
||||
@@ -15,8 +27,17 @@ require __DIR__ . '/header.php';
|
||||
?>
|
||||
|
||||
<div class="pf-container">
|
||||
<div class="pf-hero" style="text-align: center; margin-bottom: 30px;">
|
||||
<h1 style="margin-bottom: 20px;"><?= tr('budget_main_header') ?></h1>
|
||||
<div class="pf-hero budget-hero">
|
||||
<div class="budget-title-group">
|
||||
<h1><?= tr('budget_main_header') ?></h1>
|
||||
<!-- Bouton avec séparation claire entre l'engrenage animable et l'alerte fixe -->
|
||||
<button class="btn-header-settings" onclick="openBudgetSettings()" title="<?= tr('settings') ?>">
|
||||
<span class="gear-icon-anim">⚙️</span>
|
||||
<?php if (!$isBudgetSetupOk || !$hasBudgetItems): ?>
|
||||
<span class="alert-dot">!</span>
|
||||
<?php endif; ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="budget-tabs-container">
|
||||
<a href="?tab=suivi" class="tab-item <?= $tab == 'suivi' ? 'active' : '' ?>">
|
||||
@@ -38,13 +59,18 @@ require __DIR__ . '/header.php';
|
||||
<span class="tab-icon">📊</span>
|
||||
<span><?= tr('budget_tab_recap') ?></span>
|
||||
</a>
|
||||
|
||||
<a href="?tab=provisions" class="tab-item <?= $tab == 'provisions' ? 'active' : '' ?>">
|
||||
<span class="tab-icon">🧮</span>
|
||||
<span><?= tr('budget_tab_provisions') ?></span>
|
||||
</a>
|
||||
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<section class="pf-section">
|
||||
<?php
|
||||
// ROUTAGE DYNAMIQUE
|
||||
$allowedTabs = ['recap', 'suivi', 'epargne', 'budget_prev'];
|
||||
$allowedTabs = ['recap', 'suivi', 'epargne', 'budget_prev', 'provisions'];
|
||||
|
||||
if (in_array($tab, $allowedTabs)) {
|
||||
$viewPath = __DIR__ . "/modules/budget/views/" . $tab . ".php";
|
||||
@@ -52,7 +78,7 @@ require __DIR__ . '/header.php';
|
||||
if (file_exists($viewPath)) {
|
||||
include $viewPath;
|
||||
} else {
|
||||
echo "<div style='text-align:center; padding:50px; color:#ef4444;'>";
|
||||
echo "<div class='budget-error-msg'>";
|
||||
echo "<h3>" . tr('budget_err_file_not_found') . "</h3>";
|
||||
echo "<p>" . sprintf(tr('budget_err_file_detail'), htmlspecialchars($tab)) . "</p>";
|
||||
echo "</div>";
|
||||
@@ -63,26 +89,8 @@ require __DIR__ . '/header.php';
|
||||
?>
|
||||
</section>
|
||||
</div>
|
||||
<script>
|
||||
// --- AUTO-SCROLL DES ONGLETS MOBILES ---
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const tabsContainer = document.querySelector('.budget-tabs-container');
|
||||
const activeTab = document.querySelector('.tab-item.active');
|
||||
|
||||
// Si on a bien un conteneur d'onglets ET un onglet actif
|
||||
if (tabsContainer && activeTab) {
|
||||
// On vérifie si le conteneur a un scroll horizontal possible
|
||||
if (tabsContainer.scrollWidth > tabsContainer.clientWidth) {
|
||||
// On calcule la position pour centrer l'onglet à l'écran
|
||||
const scrollPos = activeTab.offsetLeft - (tabsContainer.offsetWidth / 2) + (activeTab.offsetWidth / 2);
|
||||
|
||||
// On fait glisser le menu doucement
|
||||
tabsContainer.scrollTo({
|
||||
left: scrollPos,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?php require __DIR__ . '/footer.php'; ?>
|
||||
<?php
|
||||
require __DIR__ . '/modules/budget/views/settings_modal.php';
|
||||
require __DIR__ . '/footer.php';
|
||||
?>
|
||||
@@ -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),
|
||||
|
||||
+231
-69
@@ -1,10 +1,43 @@
|
||||
-- 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',
|
||||
csv_mapping TEXT DEFAULT NULL,
|
||||
cares_modes TEXT DEFAULT NULL,
|
||||
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');
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- Paramètres dynamiques des Modules (Key-Value)
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS `pf_settings` (
|
||||
`setting_key` varchar(50) NOT NULL,
|
||||
`setting_value` text DEFAULT NULL,
|
||||
`module` varchar(50) NOT NULL,
|
||||
`updated_at` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
PRIMARY KEY (`setting_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT IGNORE INTO `pf_settings` (`setting_key`, `setting_value`, `module`) VALUES
|
||||
('calendar_default_view', 'month', 'calendar'),
|
||||
('calendar_first_day', '1', 'calendar'),
|
||||
('calendar_working_hours', '08:00-19:00', 'calendar'),
|
||||
('budget_start_day', '1', 'budget'),
|
||||
('budget_default_tab', 'dépenses', 'budget'),
|
||||
('travel_default_transport', 'car', 'voyage'),
|
||||
('travel_default_fuel_price', '1.85', 'voyage'),
|
||||
('gifts_hide_purchased', '0', 'cadeaux'),
|
||||
('gifts_default_sort', 'person', 'cadeaux'),
|
||||
('gifts_budget_alert', '500', 'cadeaux');
|
||||
|
||||
-- ─── Utilisateurs (Legacy) ────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(100) NOT NULL UNIQUE,
|
||||
@@ -12,21 +45,16 @@ CREATE TABLE IF NOT EXISTS pf_users (
|
||||
display_name VARCHAR(100) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ─── Familles ─────────────────────────────────────────────────────────────
|
||||
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;
|
||||
|
||||
-- Initialisation de la ligne unique par défaut
|
||||
INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '€', 'C');
|
||||
|
||||
-- ─── Personnes ────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_people (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL
|
||||
CREATE TABLE IF NOT EXISTS `pf_people` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(100) NOT NULL,
|
||||
`user_id` int(11) DEFAULT NULL,
|
||||
`role` varchar(50) DEFAULT NULL,
|
||||
`care_modes` varchar(255) DEFAULT NULL,
|
||||
`color` varchar(7) DEFAULT '#0891b2',
|
||||
`is_active` TINYINT(1) DEFAULT 1,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ─── Calendrier familial ──────────────────────────────────────────────────────
|
||||
@@ -64,10 +92,29 @@ CREATE TABLE IF NOT EXISTS pf_leave_snapshots (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pf_person_leave_meta (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
person_id INT NOT NULL UNIQUE,
|
||||
anniversary_date DATE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
id INT(11) AUTO_INCREMENT PRIMARY KEY,
|
||||
person_id INT(11) NOT NULL,
|
||||
leave_type VARCHAR(50) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
method VARCHAR(20) COLLATE utf8mb4_unicode_ci DEFAULT 'FIXED',
|
||||
allowance DECIMAL(5,2) DEFAULT 0.00,
|
||||
anniversary_date DATE DEFAULT NULL,
|
||||
INDEX (person_id),
|
||||
INDEX (leave_type)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ─── Types de Congés (Référentiel) ─────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_leave_types (
|
||||
id INT(11) AUTO_INCREMENT PRIMARY KEY,
|
||||
code VARCHAR(20) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
label VARCHAR(100) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
default_allowance DECIMAL(5,2) DEFAULT 0.00,
|
||||
reset_month INT(11) DEFAULT 1,
|
||||
allow_carry_over TINYINT(1) DEFAULT 0,
|
||||
UNIQUE KEY code (code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
INSERT IGNORE INTO pf_leave_types (code, label, default_allowance, reset_month, allow_carry_over) VALUES
|
||||
('CP', 'Congés Payés', 25.00, 6, 0);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pf_calendar_weeks (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
@@ -84,6 +131,19 @@ CREATE TABLE IF NOT EXISTS pf_calendar_weeks (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ─── Budget ───────────────────────────────────────────────────────────────────
|
||||
|
||||
-- NOUVEAU : Table des comptes bancaires
|
||||
CREATE TABLE IF NOT EXISTS pf_bank_accounts (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
account_type ENUM('checking', 'savings') DEFAULT 'checking',
|
||||
owner_person_id INT DEFAULT NULL,
|
||||
is_default TINYINT(1) DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (owner_person_id) REFERENCES pf_people(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pf_budget_items (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
@@ -108,13 +168,27 @@ CREATE TABLE IF NOT EXISTS pf_expenses (
|
||||
amount DECIMAL(10,2) DEFAULT 0,
|
||||
import_ref VARCHAR(255) DEFAULT NULL,
|
||||
budget_item_id INT DEFAULT NULL,
|
||||
holiday_id INT DEFAULT NULL
|
||||
holiday_id INT DEFAULT NULL,
|
||||
salary_id INT DEFAULT NULL -- 🔥 AJOUT : Lien vers le salaire configuré
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pf_expected_expenses` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`title` VARCHAR(150) NOT NULL,
|
||||
`amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`expected_date` DATE NOT NULL,
|
||||
`is_paid` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_expected_date` (`expected_date`),
|
||||
INDEX `idx_is_paid` (`is_paid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
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,
|
||||
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;
|
||||
@@ -123,9 +197,10 @@ 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 (
|
||||
@@ -134,7 +209,8 @@ CREATE TABLE IF NOT EXISTS pf_savings (
|
||||
month_date VARCHAR(10) NOT NULL,
|
||||
category VARCHAR(100) NOT NULL,
|
||||
amount DECIMAL(10,2) DEFAULT 0,
|
||||
holiday_id INT DEFAULT NULL
|
||||
holiday_id INT DEFAULT NULL,
|
||||
UNIQUE KEY uq_savings (owner, month_date, category)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pf_bank_snapshots (
|
||||
@@ -158,9 +234,38 @@ 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,
|
||||
`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(),
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ─── Budget Catégories (Dynamiques) ─────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_budget_categories (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
code VARCHAR(50) NOT NULL UNIQUE,
|
||||
label VARCHAR(100) NOT NULL,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
color VARCHAR(20) DEFAULT '#ccc',
|
||||
icon VARCHAR(20) DEFAULT '?'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
-- Insertion des catégories de base pour toute nouvelle famille
|
||||
INSERT IGNORE INTO pf_budget_categories (code, label, type, color, icon) VALUES
|
||||
('INCOME', 'Revenus', 'Income', '#10b981', '💵'),
|
||||
('FIXED', 'Charges Fixes', 'Expense', '#ef4444', '🏢');
|
||||
|
||||
-- ─── Notes / Memo ─────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_notes (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
note_type VARCHAR(100) NOT NULL,
|
||||
@@ -169,6 +274,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,
|
||||
@@ -179,7 +307,33 @@ CREATE TABLE IF NOT EXISTS pf_holidays (
|
||||
status VARCHAR(50) DEFAULT 'draft',
|
||||
budget_food DECIMAL(10,2) DEFAULT 0,
|
||||
budget_extra DECIMAL(10,2) DEFAULT 0,
|
||||
notes TEXT DEFAULT NULL
|
||||
notes TEXT DEFAULT NULL,
|
||||
vehicle_id INT DEFAULT NULL,
|
||||
return_step_id INT DEFAULT NULL,
|
||||
image_url VARCHAR(500) DEFAULT NULL,
|
||||
FOREIGN KEY (vehicle_id) REFERENCES pf_vehicles(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pf_holidays_items (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
holiday_id INT NOT NULL,
|
||||
category VARCHAR(100),
|
||||
name VARCHAR(255),
|
||||
amount DECIMAL(10,2) DEFAULT 0,
|
||||
is_paid TINYINT(1) DEFAULT 0,
|
||||
location_name VARCHAR(255) DEFAULT NULL,
|
||||
lat DECIMAL(10,7) DEFAULT NULL,
|
||||
lng DECIMAL(10,7) DEFAULT NULL,
|
||||
sort_order INT DEFAULT 0,
|
||||
notes TEXT DEFAULT NULL,
|
||||
item_date DATE DEFAULT NULL,
|
||||
item_time TIME DEFAULT NULL,
|
||||
step_type VARCHAR(20) DEFAULT 'stop',
|
||||
step_start_date DATE DEFAULT NULL,
|
||||
step_end_date DATE DEFAULT NULL,
|
||||
duration INT DEFAULT NULL,
|
||||
expense_context VARCHAR(20) DEFAULT NULL,
|
||||
FOREIGN KEY (holiday_id) REFERENCES pf_holidays(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pf_holidays_items (
|
||||
@@ -214,6 +368,17 @@ CREATE TABLE IF NOT EXISTS pf_holidays_ideas (
|
||||
notes TEXT DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pf_holidays_attachments` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`holiday_id` INT NOT NULL,
|
||||
`item_id` INT DEFAULT NULL,
|
||||
`file_name` VARCHAR(255) NOT NULL,
|
||||
`file_path` VARCHAR(255) NOT NULL,
|
||||
`uploaded_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
CONSTRAINT `fk_holiday_attachment` FOREIGN KEY (`holiday_id`) REFERENCES `pf_holidays`(`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pf_geocode_cache (
|
||||
q_hash CHAR(64) PRIMARY KEY,
|
||||
q VARCHAR(255),
|
||||
@@ -225,27 +390,42 @@ CREATE TABLE IF NOT EXISTS pf_geocode_cache (
|
||||
|
||||
-- ─── Cadeaux ──────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_gifts (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
year INT NOT NULL,
|
||||
id INT(11) AUTO_INCREMENT PRIMARY KEY,
|
||||
year INT(11) NOT NULL,
|
||||
adult_name VARCHAR(100) NOT NULL,
|
||||
payer_name VARCHAR(100),
|
||||
payer_name VARCHAR(100) DEFAULT NULL,
|
||||
child_name VARCHAR(100) NOT NULL,
|
||||
occasion VARCHAR(50) NOT NULL,
|
||||
gift_description TEXT NOT NULL,
|
||||
product_link VARCHAR(500) DEFAULT NULL,
|
||||
amount DECIMAL(8,2) DEFAULT 0
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
amount DECIMAL(8,2) DEFAULT 0.00
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
CREATE TABLE IF NOT EXISTS pf_gift_occasions (
|
||||
id INT(11) AUTO_INCREMENT PRIMARY KEY,
|
||||
code VARCHAR(50) NOT NULL UNIQUE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
icon VARCHAR(20) DEFAULT '🎁',
|
||||
month_date VARCHAR(5) DEFAULT NULL,
|
||||
is_active TINYINT(1) DEFAULT 1,
|
||||
sort_order INT DEFAULT 0
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
-- ─── 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');
|
||||
INSERT IGNORE INTO pf_gift_occasions (code, name, icon, month_date, is_active, sort_order) VALUES
|
||||
('XMAS', 'Noël', '🎄', '12-25', 1, 1),
|
||||
('BIRTHDAY', 'Anniversaire', '🎂', NULL, 1, 2),
|
||||
('OTHER', 'Autre', '🎁', NULL, 1, 99);
|
||||
|
||||
-- Personnes (IDs fixes correspondant aux constantes dans config.php)
|
||||
INSERT IGNORE INTO pf_people (id, name) VALUES (2, 'Alex');
|
||||
INSERT IGNORE INTO pf_people (id, name) VALUES (3, 'Laia');
|
||||
-- Table des règles (Qui offre à Qui)
|
||||
CREATE TABLE IF NOT EXISTS pf_gift_rules (
|
||||
id INT(11) AUTO_INCREMENT PRIMARY KEY,
|
||||
adult_person_id INT(11) NOT NULL,
|
||||
child_person_id INT(11) NOT NULL,
|
||||
occasion_id INT(11) NOT NULL,
|
||||
INDEX (adult_person_id),
|
||||
INDEX (child_person_id),
|
||||
INDEX (occasion_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
-- ─── Garage Manager ───────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_vehicles (
|
||||
@@ -257,6 +437,7 @@ CREATE TABLE IF NOT EXISTS pf_vehicles (
|
||||
license_plate VARCHAR(50) DEFAULT NULL,
|
||||
vin VARCHAR(100) DEFAULT NULL,
|
||||
fuel_type VARCHAR(50) DEFAULT 'Essence',
|
||||
consumption decimal(4,2) DEFAULT NULL COMMENT 'Consommation L/100km',
|
||||
color VARCHAR(50) DEFAULT NULL,
|
||||
purchase_date DATE DEFAULT NULL,
|
||||
purchase_price DECIMAL(10,2) DEFAULT NULL,
|
||||
@@ -321,30 +502,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,15 +529,17 @@ 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',
|
||||
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 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,
|
||||
@@ -445,6 +604,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),
|
||||
@@ -496,3 +656,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;
|
||||
@@ -1,105 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* HouseHub OS - Source Extractor 🦙🚀
|
||||
* Génère un fichier Markdown contenant tout le code source du projet.
|
||||
* Idéal pour alimenter une GEM ou un LLM.
|
||||
*/
|
||||
|
||||
// 1. 🚨 SÉCURITÉ : Ne jamais laisser ce fichier accessible publiquement sans protection !
|
||||
// Appelle le script via : https://househub.nas.../extract_source.php?key=TON_MOT_DE_PASSE
|
||||
$secretKey = 'mR83s7MmXbP$jer$9C4HnGkry6xhGL6';
|
||||
|
||||
if (!isset($_GET['key']) || $_GET['key'] !== $secretKey) {
|
||||
http_response_code(403);
|
||||
die("⛔ Accès refusé. Veuillez fournir la clé de sécurité.");
|
||||
}
|
||||
|
||||
// Forcer l'affichage en texte brut dans le navigateur pour un copier-coller facile
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
|
||||
$root = __DIR__;
|
||||
|
||||
// 2. CONFIGURATION DU FILTRAGE
|
||||
// Dossiers à exclure (ne mets pas de slash au début)
|
||||
$excludeDirs = [
|
||||
'.git',
|
||||
'.gitea',
|
||||
'.devtools',
|
||||
'vendor',
|
||||
'node_modules',
|
||||
'assets/img',
|
||||
'modules/holidays/assets/img',
|
||||
'modules/family-calendar/assets/img',
|
||||
'modules/gift-list/assets/img'
|
||||
];
|
||||
|
||||
// Fichiers spécifiques à exclure (Sécurité & Bruit)
|
||||
$excludeFiles = [
|
||||
'.env',
|
||||
'.env.example',
|
||||
'extract_source.php', // On s'exclut soi-même
|
||||
'pachafamily_source.md',
|
||||
'docker-compose.override.yml'
|
||||
];
|
||||
|
||||
// Extensions autorisées (on ignore les images, pdf, zip, etc.)
|
||||
$allowedExtensions = ['php', 'js', 'css', 'sql', 'json', 'yml', 'yaml', 'md', 'sh'];
|
||||
|
||||
// 3. INITIALISATION DU PARCOURS
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
$report = "# 🦙 Source Code HouseHub OS\n\n";
|
||||
$report .= "> *Généré le " . date('Y-m-d H:i:s') . "*\n\n";
|
||||
$report .= "---\n\n";
|
||||
|
||||
// 4. BOUCLE D'EXTRACTION
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isDir()) continue;
|
||||
|
||||
$filePath = $file->getPathname();
|
||||
$relativePath = str_replace($root . DIRECTORY_SEPARATOR, '', $filePath);
|
||||
$relativePath = str_replace('\\', '/', $relativePath); // Uniformisation Windows/Linux
|
||||
|
||||
// Extraction de l'extension
|
||||
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
||||
|
||||
// A. Filtrage par dossier
|
||||
$skip = false;
|
||||
foreach ($excludeDirs as $dir) {
|
||||
// Si le chemin commence par ce dossier, ou contient /ce_dossier/
|
||||
if (strpos($relativePath, $dir . '/') === 0 || strpos($relativePath, '/' . $dir . '/') !== false) {
|
||||
$skip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($skip) continue;
|
||||
|
||||
// B. Filtrage par nom de fichier
|
||||
if (in_array(basename($filePath), $excludeFiles)) continue;
|
||||
|
||||
// C. Filtrage par extension
|
||||
if (!in_array($ext, $allowedExtensions)) continue;
|
||||
|
||||
// 5. LECTURE ET FORMATAGE
|
||||
$content = file_get_contents($filePath);
|
||||
|
||||
if ($content === false) {
|
||||
$content = "// Erreur lors de la lecture de ce fichier.";
|
||||
}
|
||||
|
||||
// Ajustement du tag de langage pour le Markdown
|
||||
$lang = $ext;
|
||||
if ($ext === 'js') $lang = 'javascript';
|
||||
if ($ext === 'yml') $lang = 'yaml';
|
||||
|
||||
$report .= "### 📄 Fichier : `$relativePath`\n";
|
||||
$report .= "```$lang\n";
|
||||
$report .= trim($content) . "\n";
|
||||
$report .= "```\n\n---\n\n";
|
||||
}
|
||||
|
||||
// 6. SORTIE
|
||||
echo $report;
|
||||
+306
-85
@@ -8,6 +8,75 @@ require_login();
|
||||
require __DIR__ . '/includes/db.php';
|
||||
require_once __DIR__ . '/includes/i18n.php';
|
||||
|
||||
// 1. Récupération de toutes les personnes
|
||||
$stmtPeople = $pdo->query("SELECT id, name, user_id, role, color, care_modes FROM pf_people WHERE is_active = 1 ORDER BY id ASC");
|
||||
$familyPeople = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$parents = [];
|
||||
$kids = [];
|
||||
$helpers = [];
|
||||
|
||||
foreach ($familyPeople as $p) {
|
||||
$role = strtolower($p['role'] ?? '');
|
||||
if ($role === 'parent') {
|
||||
$parents[] = $p;
|
||||
} elseif ($role === 'enfant' || $role === 'child') {
|
||||
$p['modes'] = json_decode($p['care_modes'] ?? '[]', true) ?: [];
|
||||
$kids[] = $p;
|
||||
} elseif ($role === 'helper' || $role === 'nounou') {
|
||||
$helpers[] = $p; // 🟢 On stocke les intervenants
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Modes de garde (globaux du foyer, pour synchroniser avec le JS)
|
||||
$stmtFoyer = $pdo->query("SELECT care_modes, zone_scolaire FROM pf_foyer_settings LIMIT 1");
|
||||
$foyerData = $stmtFoyer->fetch(PDO::FETCH_ASSOC);
|
||||
$activeCareModes = json_decode($foyerData['care_modes'] ?? '[]', true);
|
||||
if (!is_array($activeCareModes)) $activeCareModes = [];
|
||||
|
||||
// 3. Matrice de congés dynamique (pour les en-têtes et la modale)
|
||||
$stmtLeaves = $pdo->query("SELECT person_id, leave_type FROM pf_person_leave_meta ORDER BY id ASC");
|
||||
$dbLeaves = $stmtLeaves->fetchAll(PDO::FETCH_ASSOC);
|
||||
$leaveMatrix = [];
|
||||
$allLeaveTypes = [];
|
||||
foreach ($dbLeaves as $l) {
|
||||
$leaveMatrix[$l['person_id']][] = $l['leave_type'];
|
||||
$allLeaveTypes[$l['leave_type']] = true;
|
||||
}
|
||||
$allLeaveTypes = array_keys($allLeaveTypes);
|
||||
|
||||
// ==========================================
|
||||
// VÉRIFICATIONS D'INTÉGRITÉ (ALERTES)
|
||||
// ==========================================
|
||||
$calendarAlerts = [];
|
||||
|
||||
// 1. Aucun membre adulte/parent configuré
|
||||
if (empty($parents)) {
|
||||
$calendarAlerts[] = "<strong>Aucun adulte configuré.</strong> Rendez-vous dans les Paramètres Généraux de HouseHub pour ajouter les membres du foyer.";
|
||||
}
|
||||
|
||||
// 2. Enfants présents, mais aucun mode de garde global défini
|
||||
if (!empty($kids) && empty($activeCareModes)) {
|
||||
$calendarAlerts[] = "<strong>Modes de garde manquants.</strong> Vous avez des enfants, mais aucun mode de garde n'est défini. Cliquez sur l'icône ⚙️ (Paramètres du calendrier) pour les ajouter.";
|
||||
}
|
||||
|
||||
// 3. Référentiel des types de congés vide
|
||||
try {
|
||||
$countLeaveTypes = $pdo->query("SELECT COUNT(*) FROM pf_leave_types")->fetchColumn();
|
||||
if ($countLeaveTypes == 0) {
|
||||
$calendarAlerts[] = "<strong>Types de congés manquants.</strong> Le catalogue des congés (CP, RTT...) est vide. Ajoutez-les dans les paramètres généraux.";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Si la table n'existe pas encore
|
||||
$calendarAlerts[] = "<strong>Table des congés manquante.</strong> Veuillez mettre à jour la base de données.";
|
||||
}
|
||||
|
||||
// 4. Zone scolaire manquante (si tu l'as stockée dans pf_foyer_settings)
|
||||
$zoneScolaire = $foyerData['zone_scolaire'] ?? null;
|
||||
if (empty($zoneScolaire)) {
|
||||
$calendarAlerts[] = "<strong>Zone scolaire non définie.</strong> Cliquez sur l'icône ⚙️ pour configurer votre zone afin d'afficher les vacances scolaires.";
|
||||
}
|
||||
|
||||
$pageTitle = tr('fc_page_title');
|
||||
$activePage = "family-calendar";
|
||||
$mainClass = "pf-family-calendar";
|
||||
@@ -17,10 +86,18 @@ require __DIR__ . '/header.php';
|
||||
?>
|
||||
|
||||
<div class="pf-container" style="max-width:100%; padding:0;">
|
||||
<div class="fc-header-row">
|
||||
<h1><?= tr('fc_main_header') ?></h1>
|
||||
<div class="fc-header-row" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px; margin-bottom: 24px;">
|
||||
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<!-- Bloc Titre + Roue crantée -->
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<h1 style="margin: 0;"><?= tr('fc_main_header') ?></h1>
|
||||
<button id="btnOpenCalendarSettings" class="btn-settings-gear" title="Paramètres du calendrier" style="font-size: 1.4rem; padding: 4px;">
|
||||
⚙️
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Boutons d'actions -->
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||
<button id="btnOpenSnapshotModal" class="pf-btn-icon-text">
|
||||
<span class="icon">⚖️</span>
|
||||
<span class="text"><?= tr('fc_btn_correct_balances') ?></span>
|
||||
@@ -32,6 +109,20 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($calendarAlerts)): ?>
|
||||
<div class="pf-alert pf-alert-warning" style="background: var(--bg-warning, #fffbeb); border: 1px solid var(--border-warning, #fcd34d); color: var(--text-warning, #92400e); padding: 16px; border-radius: 8px; margin-bottom: 24px; display: flex; gap: 12px; align-items: flex-start;">
|
||||
<div style="font-size: 1.5rem; line-height: 1;">⚠️</div>
|
||||
<div>
|
||||
<h4 style="margin: 0 0 8px 0; font-size: 1.05rem;">Configuration incomplète</h4>
|
||||
<ul style="margin: 0; padding-left: 18px; font-size: 0.95rem; line-height: 1.5;">
|
||||
<?php foreach ($calendarAlerts as $alert): ?>
|
||||
<li style="margin-bottom: 4px;"><?= $alert ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div id="modalHolidays" class="pf-modal">
|
||||
<div class="pf-modal-content">
|
||||
<div class="pf-modal-header">
|
||||
@@ -70,16 +161,20 @@ require __DIR__ . '/header.php';
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label"><?= tr('fc_label_person') ?></label>
|
||||
<select id="snapPerson" class="pf-input" required>
|
||||
<option value="2">Alex</option>
|
||||
<option value="3">Laia</option>
|
||||
<option value="" disabled selected>-- <?= tr('fc_choose_person') ?> --</option>
|
||||
<?php foreach ($parents as $person): ?>
|
||||
<option value="<?= htmlspecialchars($person['id']) ?>">
|
||||
<?= htmlspecialchars($person['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label"><?= tr('fc_label_leave_type') ?></label>
|
||||
<select id="snapType" class="pf-input" required>
|
||||
<option value="CP">CP</option>
|
||||
<option value="JRA">JRA</option>
|
||||
<option value="JA">JA</option>
|
||||
<?php foreach ($allLeaveTypes as $type): ?>
|
||||
<option value="<?= htmlspecialchars($type) ?>"><?= htmlspecialchars($type) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,32 +199,142 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="modalCalendarSettings" class="pf-modal">
|
||||
<div class="pf-modal-content" style="max-width: 650px; width: 95%;">
|
||||
|
||||
<div class="pf-modal-header">
|
||||
<h3 class="pf-modal-title">⚙️ <?= tr('fc_modal_settings_title') ?></h3>
|
||||
<button type="button" class="pf-modal-close" onclick="closeCalendarSettings()">×</button>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 4px; background: var(--bg-page); padding: 4px; border-radius: 8px; margin: 1rem;">
|
||||
<button type="button" id="tab-btn-foyer" class="bs-tab-btn active" style="flex: 1; padding: 8px 12px; font-weight: 600;" onclick="switchCalendarTab('foyer')">
|
||||
🏡 <?= tr('fc_tab_foyer') ?>
|
||||
</button>
|
||||
<button type="button" id="tab-btn-membres" class="bs-tab-btn" style="flex: 1; padding: 8px 12px; font-weight: 600;" onclick="switchCalendarTab('membres')">
|
||||
👥 <?= tr('fc_tab_members') ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="pf-modal-body" style="max-height: 65vh; overflow-y: auto; padding-top: 0; padding-bottom: 1rem;">
|
||||
|
||||
<div id="cal-pane-foyer" class="cal-settings-pane">
|
||||
|
||||
<form id="formCalFoyer" onsubmit="submitCalFoyer(event)">
|
||||
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label"><?= tr('fc_zone_label') ?></label>
|
||||
<select id="setZoneScolaire" name="zone_scolaire" class="pf-input" required style="width: 100%;">
|
||||
<option value="A"><?= tr('fc_zone_a') ?></option>
|
||||
<option value="B"><?= tr('fc_zone_b') ?></option>
|
||||
<option value="C"><?= tr('fc_zone_c') ?></option>
|
||||
<option value="Autre"><?= tr('fc_zone_other') ?></option>
|
||||
</select>
|
||||
<small class="pf-muted-note"><?= tr('fc_zone_desc') ?></small>
|
||||
</div>
|
||||
|
||||
<div class="pf-form-group" style="margin-top: 1.5rem;">
|
||||
<label class="pf-label"><?= tr('fc_care_modes_label') ?></label>
|
||||
|
||||
<?php if (!empty($kids)): ?>
|
||||
<div id="careModesContainer" style="display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; min-height: 38px; padding: 8px; background: var(--bg-page); border-radius: var(--radius); border: 1px solid var(--border-light);"></div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<input type="text" id="inputNewCareMode" class="pf-input" placeholder="<?= tr('fc_add_mode_placeholder') ?>" style="flex: 1;">
|
||||
<button type="button" class="pf-btn btn-secondary" style="padding: 0 16px; border-radius: 6px;" onclick="addCareModeTag()">+</button>
|
||||
</div>
|
||||
<small class="pf-muted-note"><?= tr('fc_care_modes_desc') ?></small>
|
||||
<?php else: ?>
|
||||
<div style="background: var(--bg-subtle); padding: 12px; border-radius: 8px; border: 1px dashed var(--border-main); text-align: center;">
|
||||
<p style="margin: 0 0 10px 0; color: var(--text-muted); font-size: 0.9rem;">
|
||||
<?= tr('fc_empty_kids_msg') ?>
|
||||
</p>
|
||||
<a href="/settings.php" class="pf-btn btn-secondary" style="text-decoration: none; display: inline-block; font-size: 0.85rem;">
|
||||
⚙️ <?= tr('fc_link_settings') ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($kids)): ?>
|
||||
<div style="margin-top: 1rem; display: flex; justify-content: flex-end;">
|
||||
<button type="submit" class="pf-btn pf-btn-primary"><?= tr('save') ?></button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
|
||||
<div class="pf-form-group" style="margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--border-light);">
|
||||
<h4 style="margin: 0 0 10px 0; color: var(--text-main);">🏖️ <?= tr('fc_leave_catalog_title') ?></h4>
|
||||
<p style="font-size: 0.85rem; color: var(--text-muted); margin-bottom: 15px;">
|
||||
<?= tr('fc_leave_catalog_desc') ?>
|
||||
</p>
|
||||
|
||||
<div id="leaveTypesContainer" style="display: flex; flex-direction: column; gap: 8px; margin-bottom: 15px;"></div>
|
||||
|
||||
<div style="background: var(--bg-subtle); padding: 12px; border-radius: 8px; border: 1px solid var(--border-light);">
|
||||
<h5 id="leaveTypeFormTitle" style="margin: 0 0 10px 0;">+ <?= tr('fc_add_leave_type') ?></h5>
|
||||
|
||||
<form id="formLeaveType" style="display: flex; flex-direction: column; gap: 10px;">
|
||||
<input type="hidden" id="lt-mode" value="add">
|
||||
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||
<div style="flex: 0 0 80px;">
|
||||
<label class="pf-label"><?= tr('fc_col_code') ?> *</label>
|
||||
<input type="text" id="lt-code" class="pf-input" maxlength="4" style="text-transform: uppercase;" required>
|
||||
<small class="pf-muted-note" id="lt-code-note"><?= tr('fc_code_irreversible') ?></small>
|
||||
</div>
|
||||
<div style="flex: 1; min-width: 150px;">
|
||||
<label class="pf-label"><?= tr('fc_col_label') ?> *</label>
|
||||
<input type="text" id="lt-label" class="pf-input" placeholder="<?= tr('fc_leave_label_ph') ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 5px;">
|
||||
<button type="button" class="pf-btn btn-secondary" onclick="resetLeaveTypeForm()"><?= tr('btn_cancel') ?></button>
|
||||
<button type="button" class="pf-btn pf-btn-primary" onclick="saveLeaveType()"><?= tr('btn_save') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="cal-pane-membres" class="cal-settings-pane" style="display: none;">
|
||||
|
||||
<div style="margin-bottom: 1.2rem;">
|
||||
<label class="pf-label"><?= tr('fc_select_member') ?></label>
|
||||
<select id="selectCalMember" class="pf-input" style="width: 100%; font-weight: 600;" onchange="loadMemberConfigView()">
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="memberConfigZone" style="background: var(--bg-page); padding: 14px; border-radius: 8px; border: 1px solid var(--border-light); min-height: 150px;">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="pf-section">
|
||||
<div class="fc-month-calendar-wrapper">
|
||||
|
||||
<div class="fc-month-header">
|
||||
|
||||
<div class="fc-month-nav-row">
|
||||
<button id="fc-prev-month" class="fc-nav-button">‹</button>
|
||||
|
||||
<div id="fc-smart-date-selector" style="display:flex; align-items:center; justify-content:center; flex-grow:1; gap:6px;">
|
||||
<select id="fc-select-month" class="fc-smart-select"></select>
|
||||
<select id="fc-select-year" class="fc-smart-select"></select>
|
||||
<span id="fc-multi-month-suffix" style="display:none; font-size:1.3rem; font-weight:800; color:#0f172a; margin-left:4px;"></span>
|
||||
</div>
|
||||
|
||||
<button id="fc-next-month" class="fc-nav-button">›</button>
|
||||
</div>
|
||||
|
||||
<div class="fc-view-controls">
|
||||
<button id="fc-today-btn" class="fc-today-button" title="<?= tr('fc_today_title') ?>"><?= tr('fc_today_short') ?></button> <div class="fc-view-divider"></div>
|
||||
<button id="fc-today-btn" class="fc-today-button" title="<?= tr('fc_today_title') ?>"><?= tr('fc_today_short') ?></button>
|
||||
<div class="fc-view-divider"></div>
|
||||
<button class="fc-view-button fc-view-button--active" data-view="1month"><?= tr('fc_view_1m') ?></button>
|
||||
<button class="fc-view-button" data-view="2months"><?= tr('fc_view_2m') ?></button>
|
||||
<button class="fc-view-button" data-view="3months"><?= tr('fc_view_3m') ?></button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="fc-calendar-container">
|
||||
@@ -138,11 +343,9 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
|
||||
<div id="fc-month-balances" class="fc-month-balances"></div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="pf-section">
|
||||
<section class="pf-section" style="margin-top: 1.5rem;">
|
||||
<div class="fc-week-header">
|
||||
<h2><?= tr('fc_weekly_planning') ?></h2>
|
||||
<div class="fc-week-nav-controls">
|
||||
@@ -163,26 +366,45 @@ require __DIR__ . '/header.php';
|
||||
<th rowspan="3" class="col-day"><?= tr('day_wed') ?></th>
|
||||
<th rowspan="3" class="col-day"><?= tr('day_thu') ?></th>
|
||||
<th rowspan="3" class="col-day"><?= tr('day_fri') ?></th>
|
||||
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_off_carole') ?></span></th>
|
||||
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_extra_off') ?></span></th>
|
||||
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_centre') ?></span></th>
|
||||
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_avis') ?></span></th>
|
||||
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_pep_sick') ?></span></th>
|
||||
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_presence') ?></span></th>
|
||||
<th colspan="6" class="col-alex header-group">ALEX</th>
|
||||
<th colspan="6" class="col-laia header-group">LAIA</th>
|
||||
|
||||
<?php foreach ($activeCareModes as $mode): ?>
|
||||
<th rowspan="3" class="col-total rotated-text"><span><?= htmlspecialchars($mode) ?></span></th>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php foreach ($kids as $kid): ?>
|
||||
<th rowspan="3" class="col-total rotated-text"><span>Maladie <?= htmlspecialchars($kid['name']) ?></span></th>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php foreach ($parents as $index => $parent):
|
||||
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia'; // Couleurs alternées
|
||||
$pLeaves = $leaveMatrix[$parent['id']] ?? [];
|
||||
$colspan = count($pLeaves) * 2;
|
||||
?>
|
||||
<?php if ($colspan > 0): ?>
|
||||
<th colspan="<?= $colspan ?>" class="<?= $parentClass ?> header-group"><?= htmlspecialchars(strtoupper($parent['name'])) ?></th>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="2" class="col-alex-sub">CP</th><th colspan="2" class="col-alex-sub">JRA</th><th colspan="2" class="col-alex-sub">JA</th>
|
||||
<th colspan="2" class="col-laia-sub">CP</th><th colspan="2" class="col-laia-sub">JRA</th><th colspan="2" class="col-laia-sub">JA</th>
|
||||
<?php foreach ($parents as $index => $parent):
|
||||
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia';
|
||||
$pLeaves = $leaveMatrix[$parent['id']] ?? [];
|
||||
?>
|
||||
<?php foreach ($pLeaves as $type): ?>
|
||||
<th colspan="2" class="<?= $parentClass ?>-sub"><?= htmlspecialchars($type) ?></th>
|
||||
<?php endforeach; ?>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="col-alex-sub col-alex-av"><?= tr('fc_col_av') ?></th><th class="col-alex-sub col-alex-use"><?= tr('fc_col_use') ?></th>
|
||||
<th class="col-alex-sub col-alex-av"><?= tr('fc_col_av') ?></th><th class="col-alex-sub col-alex-use"><?= tr('fc_col_use') ?></th>
|
||||
<th class="col-alex-sub col-alex-av"><?= tr('fc_col_av') ?></th><th class="col-alex-sub col-alex-use"><?= tr('fc_col_use') ?></th>
|
||||
<th class="col-laia-sub col-laia-av"><?= tr('fc_col_av') ?></th><th class="col-laia-sub col-laia-use"><?= tr('fc_col_use') ?></th>
|
||||
<th class="col-laia-sub col-laia-av"><?= tr('fc_col_av') ?></th><th class="col-laia-sub col-laia-use"><?= tr('fc_col_use') ?></th>
|
||||
<th class="col-laia-sub col-laia-av"><?= tr('fc_col_av') ?></th><th class="col-laia-sub col-laia-use"><?= tr('fc_col_use') ?></th>
|
||||
<?php foreach ($parents as $index => $parent):
|
||||
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia';
|
||||
$pLeaves = $leaveMatrix[$parent['id']] ?? [];
|
||||
?>
|
||||
<?php foreach ($pLeaves as $type): ?>
|
||||
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-av"><?= tr('fc_col_av') ?></th>
|
||||
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-use"><?= tr('fc_col_use') ?></th>
|
||||
<?php endforeach; ?>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="planningBody"></tbody>
|
||||
@@ -191,37 +413,52 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="pf-section pf-section--bottom-panels">
|
||||
<div class="fc-bottom-grid">
|
||||
|
||||
<div class="pf-card">
|
||||
<div class="pf-card-title fc-summary-header">
|
||||
<span><?= tr('fc_summary_title') ?></span>
|
||||
<div class="fc-summary-controls">
|
||||
<select id="summType" class="fc-summ-select">
|
||||
<option value="year"><?= tr('fc_summ_year') ?></option>
|
||||
<option value="month"><?= tr('fc_summ_month') ?></option>
|
||||
</select>
|
||||
<select id="summValue" class="fc-summ-select"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-card-body">
|
||||
<div id="globalSummary" class="fc-summary-vertical"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="pf-section pf-section--bottom-panels" style="margin-top: 1.5rem;">
|
||||
<div class="pf-card">
|
||||
<h2 class="pf-card-title"><?= tr('fc_legend_title') ?></h2>
|
||||
<div class="pf-card-body">
|
||||
<div class="pf-legend-grid">
|
||||
<!-- Fériés & Vacances (Fixes) -->
|
||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-school-holiday"></div><span><?= tr('leg_school_holidays') ?></span></div>
|
||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-public-holiday"></div><span><?= tr('leg_public_holiday') ?></span></div>
|
||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-off-carole"></div><span><?= tr('leg_off_carole') ?></span></div>
|
||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-extra-off-carole"></div><span><?= tr('leg_extra_off') ?></span></div>
|
||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-centre"></div><span><?= tr('leg_centre') ?></span></div>
|
||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-avis"></div><span><?= tr('leg_avis') ?></span></div>
|
||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-pep-sick"></div><span><?= tr('leg_pep_sick') ?></span></div>
|
||||
|
||||
<!-- Intervenants (Dynamique) -->
|
||||
<?php foreach ($helpers as $helper): ?>
|
||||
<div class="pf-legend-item"><div class="pf-legend-color" style="background: var(--warning);"></div><span>Off <?= htmlspecialchars($helper['name']) ?></span></div>
|
||||
<div class="pf-legend-item"><div class="pf-legend-color" style="background: var(--danger);"></div><span>Extra <?= htmlspecialchars($helper['name']) ?></span></div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<!-- Modes de garde (Dynamique avec icônes) -->
|
||||
<?php foreach ($activeCareModes as $index => $mode):
|
||||
$modeLower = strtolower($mode);
|
||||
if ($modeLower === 'avis'): ?>
|
||||
<div class="pf-legend-item">
|
||||
<img src="/modules/family-calendar/assets/img/avis.svg" style="width:16px;height:16px;object-fit:contain;margin-right:6px;" alt="Avis">
|
||||
<span><?= htmlspecialchars($mode) ?></span>
|
||||
</div>
|
||||
<?php elseif ($modeLower === 'centre'): ?>
|
||||
<div class="pf-legend-item">
|
||||
<span style="font-size:1.1rem; line-height:1; margin-right:6px;">🏫</span>
|
||||
<span><?= htmlspecialchars($mode) ?></span>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="pf-legend-item">
|
||||
<div class="pf-legend-color" style="background: var(--primary);"></div>
|
||||
<span><?= htmlspecialchars($mode) ?></span>
|
||||
</div>
|
||||
<?php endif;
|
||||
endforeach; ?>
|
||||
|
||||
<!-- Enfants Malades (Dynamique avec couleur BDD) -->
|
||||
<?php foreach ($kids as $kid):
|
||||
// Fallback aligné exactement sur celui du JS (#e11d48)
|
||||
$color = !empty($kid['color']) ? $kid['color'] : '#e11d48';
|
||||
?>
|
||||
<div class="pf-legend-item">
|
||||
<div class="pf-legend-color" style="background: <?= htmlspecialchars($color) ?>; opacity: 0.8;"></div>
|
||||
<span>Maladie <?= htmlspecialchars($kid['name']) ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -229,33 +466,17 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.I18N = {
|
||||
...(window.I18N || {}),
|
||||
'fc_menu_carole': "<?= tr('fc_menu_carole') ?>",
|
||||
'btn_off': "<?= tr('btn_off') ?>",
|
||||
'btn_extra': "<?= tr('btn_extra') ?>",
|
||||
'leg_centre': "<?= tr('leg_centre') ?>",
|
||||
'leg_avis': "<?= tr('leg_avis') ?>",
|
||||
'leg_pep_sick': "<?= tr('leg_pep_sick') ?>",
|
||||
'leg_off_carole': "<?= tr('leg_off_carole') ?>",
|
||||
'leg_extra_off': "<?= tr('leg_extra_off') ?>",
|
||||
'leg_presence': "<?= tr('leg_presence') ?>",
|
||||
'fc_menu_kids_leaves': "<?= tr('fc_menu_kids_leaves') ?>",
|
||||
'fc_clear': "<?= tr('fc_clear') ?>",
|
||||
'fc_unit_days': "<?= tr('fc_unit_days') ?>",
|
||||
'vac_toussaint': "<?= tr('vac_toussaint') ?>",
|
||||
'vac_noel': "<?= tr('vac_noel') ?>",
|
||||
'vac_hiver': "<?= tr('vac_hiver') ?>",
|
||||
'vac_printemps': "<?= tr('vac_printemps') ?>",
|
||||
'vac_ascension': "<?= tr('vac_ascension') ?>",
|
||||
'vac_ete': "<?= tr('vac_ete') ?>",
|
||||
'leg_school_holidays': "<?= tr('leg_school_holidays') ?>",
|
||||
'fc_school_year': "<?= tr('fc_school_year') ?>",
|
||||
'fc_modal_holidays_title': "<?= tr('fc_modal_holidays_title') ?>",
|
||||
'fc_alert_burn_days': "<?= tr('fc_alert_burn_days') ?>",
|
||||
'fc_alert_burn_jra': "<?= tr('fc_alert_burn_jra') ?>",
|
||||
'ANNIV': "<?= tr('ANNIV') ?>"
|
||||
window.FAMILY_CONFIG = {
|
||||
parents: <?= json_encode($parents) ?>,
|
||||
kids: <?= json_encode($kids) ?>,
|
||||
activeCareModes: <?= json_encode($activeCareModes) ?>,
|
||||
i18n: {
|
||||
settings_updated: <?= json_encode(tr('fc_settings_updated')) ?>,
|
||||
empty_matrix: "Aucun compteur de congés défini pour le moment.",
|
||||
btn_delete: <?= json_encode(tr('btn_delete') ?? 'Supprimer') ?>
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<script src="/modules/family-calendar/family-calendar.js"></script>
|
||||
|
||||
<script src="/modules/family-calendar/family-calendar.js?v=<?= time() ?>"></script>
|
||||
<?php require __DIR__ . '/footer.php'; ?>
|
||||
+11
-1
@@ -217,15 +217,25 @@ require __DIR__ . '/header.php';
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label class="form-label">Kilométrage</label><input class="form-control" id="vehicle-current-km" name="current_km" type="number" placeholder="85000"></div>
|
||||
<div class="form-group"><label class="form-label">Couleur</label><input class="form-control" id="vehicle-color" name="color" placeholder="Blanc nacré"></div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Conso. moy. (L/100km)</label>
|
||||
<input class="form-control" id="vehicle-consumption" name="consumption" type="number" step="0.1" min="0" placeholder="ex: 6.5">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label class="form-label">Date d'achat</label><input class="form-control" id="vehicle-purchase-date" name="purchase_date" type="date"></div>
|
||||
<div class="form-group"><label class="form-label">Prix d'achat (€)</label><input class="form-control" id="vehicle-purchase-price" name="purchase_price" type="number" step="0.01"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label class="form-label">VIN</label><input class="form-control" id="vehicle-vin" name="vin" placeholder="VF1..."></div>
|
||||
<div class="form-group"><label class="form-label">Couleur</label><input class="form-control" id="vehicle-color" name="color" placeholder="Blanc nacré"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Photo</label>
|
||||
<input class="form-control" id="vehicle-photo" name="photo" type="file" accept="image/*" onchange="previewPhoto('vehicle-photo','vehicle-photo-preview')">
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/meta_db.php';
|
||||
|
||||
$db_host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||
$db_user = getenv('DB_USER') ?: 'househub';
|
||||
$db_pass = getenv('DB_PASS') ?: 'househub_dev';
|
||||
|
||||
echo "<h1>📅 Génération du squelette du calendrier (pf_calendar_weeks)</h1>";
|
||||
|
||||
$monthsFr = [1=>'Janvier', 2=>'Février', 3=>'Mars', 4=>'Avril', 5=>'Mai', 6=>'Juin', 7=>'Juillet', 8=>'Août', 9=>'Septembre', 10=>'Octobre', 11=>'Novembre', 12=>'Décembre'];
|
||||
|
||||
try {
|
||||
$stmt = $meta_pdo->query("SELECT db_name, name FROM families WHERE is_active = 1");
|
||||
$families = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($families as $family) {
|
||||
$dbName = $family['db_name'];
|
||||
echo "<h3>Famille : {$family['name']} ($dbName)</h3><ul>";
|
||||
|
||||
try {
|
||||
$pdo = new PDO("mysql:host=$db_host;dbname=$dbName;charset=utf8mb4", $db_user, $db_pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||
|
||||
// On utilise REPLACE INTO pour écraser proprement si certaines semaines existent déjà
|
||||
$sql = "REPLACE INTO pf_calendar_weeks
|
||||
(year, week_iso_year, week_iso_number, week_label, month, month_name, week_start_date, mon_date, tue_date, wed_date, thu_date, fri_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
$insertStmt = $pdo->prepare($sql);
|
||||
|
||||
// On génère de l'été 2023 jusqu'à fin 2030 !
|
||||
$startDate = new DateTime('2023-08-28'); // Un lundi
|
||||
$endDate = new DateTime('2030-12-31');
|
||||
$count = 0;
|
||||
|
||||
while ($startDate <= $endDate) {
|
||||
$mon = clone $startDate;
|
||||
$tue = clone $startDate; $tue->modify('+1 day');
|
||||
$wed = clone $startDate; $wed->modify('+2 days');
|
||||
$thu = clone $startDate; $thu->modify('+3 days');
|
||||
$fri = clone $startDate; $fri->modify('+4 days');
|
||||
|
||||
$year = (int)$mon->format('Y');
|
||||
$month = (int)$mon->format('n');
|
||||
$weekIsoYear = (int)$mon->format('o');
|
||||
$weekIsoNumber = (int)$mon->format('W');
|
||||
$weekLabel = "Semaine " . $weekIsoNumber;
|
||||
$monthName = $monthsFr[$month];
|
||||
|
||||
$insertStmt->execute([
|
||||
$year,
|
||||
$weekIsoYear,
|
||||
$weekIsoNumber,
|
||||
$weekLabel,
|
||||
$month,
|
||||
$monthName,
|
||||
$mon->format('Y-m-d'),
|
||||
$mon->format('Y-m-d'),
|
||||
$tue->format('Y-m-d'),
|
||||
$wed->format('Y-m-d'),
|
||||
$thu->format('Y-m-d'),
|
||||
$fri->format('Y-m-d')
|
||||
]);
|
||||
|
||||
$startDate->modify('+1 week');
|
||||
$count++;
|
||||
}
|
||||
echo "<li>✅ $count semaines générées (de 2023 à 2030) !</li>";
|
||||
} catch (\PDOException $e) {
|
||||
echo "<li>❌ Erreur : " . $e->getMessage() . "</li>";
|
||||
}
|
||||
echo "</ul>";
|
||||
}
|
||||
echo "<h2>🎉 Terminé ! Tu peux supprimer ce fichier et rafraîchir ton calendrier.</h2>";
|
||||
|
||||
} catch (Exception $e) {
|
||||
die("Erreur fatale Meta DB : " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
@@ -1,59 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* HouseHub - CSS Auditor 🦙
|
||||
* Génère un rapport complet de tous les styles (fichiers, balises, inline)
|
||||
*/
|
||||
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
|
||||
$root = __DIR__;
|
||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root));
|
||||
$report = "=== 🦙 RAPPORT CSS COMPLET HOUSEHUB ===\n";
|
||||
$report .= "Généré le : " . date('Y-m-d H:i:s') . "\n\n";
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isDir()) continue;
|
||||
|
||||
$filePath = $file->getPathname();
|
||||
$relativePath = str_replace($root, '', $filePath);
|
||||
$ext = pathinfo($filePath, PATHINFO_EXTENSION);
|
||||
|
||||
// 1. EXTRACTION DES FICHIERS .css
|
||||
if ($ext === 'css') {
|
||||
$report .= "/* 📄 FICHIER SOURCE : $relativePath */\n";
|
||||
$report .= file_get_contents($filePath) . "\n";
|
||||
$report .= str_repeat("-", 40) . "\n\n";
|
||||
}
|
||||
|
||||
// 2. EXTRACTION DES BALISES <style> ET ATTRIBUTS style="" DANS LES .php
|
||||
if ($ext === 'php') {
|
||||
$content = file_get_contents($filePath);
|
||||
$foundInFile = false;
|
||||
|
||||
// Extraction des blocs <style>
|
||||
if (preg_match_all('/<style[^>]*>(.*?)<\/style>/is', $content, $matches)) {
|
||||
if (!$foundInFile) { $report .= "/* 📄 FICHIER SOURCE : $relativePath */\n"; $foundInFile = true; }
|
||||
foreach ($matches[1] as $idx => $styleBlock) {
|
||||
$report .= "/* Bloc <style> #$idx */\n";
|
||||
$report .= trim($styleBlock) . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Extraction des styles inline style="..."
|
||||
// On capture le tag pour donner du contexte au style inline
|
||||
if (preg_match_all('/<([a-z0-9]+)[^>]*?\sstyle=["\']([^"]*?)["\'][^>]*>/i', $content, $matches)) {
|
||||
if (!$foundInFile) { $report .= "/* 📄 FICHIER SOURCE : $relativePath */\n"; $foundInFile = true; }
|
||||
foreach ($matches[0] as $idx => $fullTag) {
|
||||
// On nettoie un peu pour ne garder que l'essentiel du tag et son style
|
||||
$report .= "/* Style Inline #$idx */\n";
|
||||
$report .= trim($fullTag) . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
if ($foundInFile) {
|
||||
$report .= str_repeat("-", 40) . "\n\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo $report;
|
||||
@@ -1,72 +0,0 @@
|
||||
<?php
|
||||
// generate_prompt.php - Script pour extraire le code de HouseHub
|
||||
// À lancer via le navigateur (http://localhost:8082/generate_prompt.php) ou en ligne de commande.
|
||||
|
||||
$rootDir = __DIR__;
|
||||
$outputFile = $rootDir . '/househub_source.md';
|
||||
|
||||
// Dossiers à ignorer (pour ne pas polluer le cerveau de l'IA avec des choses inutiles)
|
||||
$ignoredDirs = ['.git', '.devtools', '.gitea', 'assets'];
|
||||
|
||||
// Extensions autorisées (on ne veut que du code, pas d'images)
|
||||
$allowedExtensions = ['php', 'css', 'js', 'html', 'sh'];
|
||||
|
||||
$output = "# 🦙 Source Code HouseHub\n\n";
|
||||
$output .= "> *Généré le " . date('Y-m-d H:i:s') . "*\n\n";
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($rootDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
$fileCount = 0;
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile()) {
|
||||
// Chemin relatif propre
|
||||
$relativePath = str_replace($rootDir . DIRECTORY_SEPARATOR, '', $file->getPathname());
|
||||
$relativePath = str_replace('\\', '/', $relativePath); // Uniformiser pour Windows
|
||||
|
||||
// Vérification des dossiers ignorés
|
||||
$skip = false;
|
||||
foreach ($ignoredDirs as $ignored) {
|
||||
if (strpos($relativePath, $ignored) === 0) {
|
||||
$skip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($skip) continue;
|
||||
|
||||
// Vérification de l'extension
|
||||
$ext = strtolower($file->getExtension());
|
||||
if (!in_array($ext, $allowedExtensions)) continue;
|
||||
|
||||
// On ignore ce script lui-même
|
||||
if ($file->getFilename() === 'generate_prompt.php') continue;
|
||||
|
||||
// Lecture du contenu
|
||||
$content = file_get_contents($file->getPathname());
|
||||
|
||||
// Détermination du langage pour le bloc Markdown
|
||||
$lang = $ext === 'php' ? 'php' : ($ext === 'js' ? 'javascript' : $ext);
|
||||
|
||||
// Formatage pour l'IA
|
||||
$output .= "### 📄 Fichier : `" . $relativePath . "`\n";
|
||||
$output .= "```" . $lang . "\n";
|
||||
$output .= $content . "\n";
|
||||
$output .= "```\n\n";
|
||||
$output .= "---\n\n";
|
||||
|
||||
$fileCount++;
|
||||
}
|
||||
}
|
||||
|
||||
file_put_contents($outputFile, $output);
|
||||
|
||||
echo "<div style='font-family:sans-serif; padding:20px; background:#f0fdf4; color:#16a34a; border-radius:8px; border:1px solid #bbf7d0;'>";
|
||||
echo "<h2>✅ Succès !</h2>";
|
||||
echo "<p>Le fichier <strong>househub_source.md</strong> a été généré à la racine de ton projet.</p>";
|
||||
echo "<p><strong>$fileCount fichiers</strong> de code ont été fusionnés.</p>";
|
||||
echo "<p><em>Tu peux maintenant donner ce fichier à ta Gem HouseHub !</em></p>";
|
||||
echo "</div>";
|
||||
?>
|
||||
+519
-79
@@ -8,51 +8,36 @@ require_once __DIR__ . '/includes/i18n.php';
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) { session_start(); }
|
||||
|
||||
// --- 1. CONFIGURATION ---
|
||||
// --- 1. CONFIGURATION DE BASE ---
|
||||
$year = (int)date('Y');
|
||||
$pageTitle = tr('gift_page_title');
|
||||
$activePage = "gift-list";
|
||||
$mainClass = "pf-gift-list";
|
||||
$pageCss = "/modules/gift-list/gift-list.css";
|
||||
|
||||
$children = ['Pol', 'Pep', 'Elna', 'Bru', 'Guim'];
|
||||
$baseAdults = ['Laia', 'Laura', 'Avi Iaia'];
|
||||
$extraAdults = ['Pauline', 'Papy JC', 'Mamy Caro'];
|
||||
// --- 2. RÉCUPÉRATION DYNAMIQUE DES DONNÉES ---
|
||||
// On inclut les enfants du foyer ET les proches enfants
|
||||
$stmt = $pdo->query("SELECT name FROM pf_people WHERE role IN ('enfant', 'proche_enfant') ORDER BY name ASC");
|
||||
$children = $stmt->fetchAll(PDO::FETCH_COLUMN) ?: ['Aucun enfant configuré'];
|
||||
|
||||
$adultsByChildForAnniv = [
|
||||
'Pol' => array_merge($baseAdults, $extraAdults),
|
||||
'Pep' => array_merge($baseAdults, $extraAdults),
|
||||
'Elna' => $baseAdults,
|
||||
'Bru' => $baseAdults,
|
||||
'Guim' => $baseAdults,
|
||||
];
|
||||
// On inclut les adultes du foyer ET les proches adultes (On exclut nounou et les enfants)
|
||||
$stmt = $pdo->query("SELECT name FROM pf_people WHERE role NOT IN ('enfant', 'proche_enfant', 'nounou') ORDER BY name ASC");
|
||||
$allAdultsList = $stmt->fetchAll(PDO::FETCH_COLUMN) ?: ['Aucun adulte configuré'];
|
||||
|
||||
$VIEWS = [
|
||||
'nadal' => ['TIO', 'NOEL', 'ROIS'],
|
||||
'anniversary' => ['ANNIV', 'SANT'],
|
||||
];
|
||||
// On récupère toutes les occasions (avec l'icône)
|
||||
$stmt = $pdo->query("SELECT code, name, icon, month_date FROM pf_gift_occasions WHERE is_active = 1 ORDER BY sort_order ASC, month_date ASC, id ASC");
|
||||
$activeOccasions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$currentView = strtolower($_GET['view'] ?? ($_SESSION['gift_view'] ?? 'nadal'));
|
||||
if (!isset($VIEWS[$currentView])) $currentView = 'nadal';
|
||||
$_SESSION['gift_view'] = $currentView;
|
||||
$allowedOccasions = $VIEWS[$currentView];
|
||||
if (empty($activeOccasions)) {
|
||||
$activeOccasions = [['code' => 'DEMO', 'name' => 'Fête par défaut', 'icon' => '🎁', 'month_date' => null]];
|
||||
}
|
||||
$allowedOccasionCodes = array_column($activeOccasions, 'code');
|
||||
|
||||
$allOccasionLabels = [
|
||||
'TIO' => tr('gift_occ_tio'), 'NOEL' => tr('gift_occ_noel'), 'ROIS' => tr('gift_occ_rois'),
|
||||
'ANNIV' => tr('gift_occ_anniv'), 'SANT' => tr('gift_occ_sant')
|
||||
];
|
||||
$occasionIcons = [
|
||||
'TIO' => '/modules/gift-list/assets/img/tio.png', 'NOEL' => '/modules/gift-list/assets/img/santa.png',
|
||||
'ROIS' => '/modules/gift-list/assets/img/reis.png', 'ANNIV' => '/modules/gift-list/assets/img/corona.png',
|
||||
'SANT' => '/modules/gift-list/assets/img/sant.png'
|
||||
];
|
||||
|
||||
// --- 2. DONNÉES ---
|
||||
$inMarks = implode(',', array_fill(0, count($allowedOccasions), '?'));
|
||||
// Tri: Par Fête, puis Enfant, puis Adulte
|
||||
// --- 3. CHARGEMENT DES CADEAUX ---
|
||||
$inMarks = implode(',', array_fill(0, count($allowedOccasionCodes), '?'));
|
||||
$sql = "SELECT * FROM pf_gifts WHERE year = ? AND occasion IN ($inMarks) ORDER BY occasion ASC, child_name ASC, adult_name ASC";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute(array_merge([$year], $allowedOccasions));
|
||||
$stmt->execute(array_merge([$year], $allowedOccasionCodes));
|
||||
$gifts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$data = [];
|
||||
@@ -62,12 +47,16 @@ foreach ($gifts as $g) {
|
||||
$data[$g['occasion']][$g['child_name']]['gifts'][] = $g;
|
||||
$data[$g['occasion']][$g['child_name']]['totals'][$g['adult_name']] = ($data[$g['occasion']][$g['child_name']]['totals'][$g['adult_name']] ?? 0) + $g['amount'];
|
||||
$adultsInView[$g['adult_name']] = true;
|
||||
if (!empty($g['payer_name'])) {
|
||||
$adultsInView[$g['payer_name']] = true;
|
||||
}
|
||||
}
|
||||
$allAdultsList = array_keys($adultsInView); sort($allAdultsList);
|
||||
|
||||
// --- 3. TRICOUNT ---
|
||||
$people = array_values(array_unique(array_merge($baseAdults, array_column($gifts, 'adult_name'), array_column($gifts, 'payer_name'))));
|
||||
// --- 4. TRICOUNT (Bilan et Remboursements) ---
|
||||
$people = array_values(array_unique(array_merge($allAdultsList, array_keys($adultsInView))));
|
||||
$people = array_filter($people);
|
||||
sort($people);
|
||||
|
||||
$matrix = [];
|
||||
foreach ($people as $p1) { foreach ($people as $p2) $matrix[$p1][$p2] = 0.0; }
|
||||
|
||||
@@ -94,18 +83,19 @@ for ($i = 0; $i < $countPeople; $i++) {
|
||||
require __DIR__ . '/header.php';
|
||||
?>
|
||||
|
||||
<div class="pf-container cl-view-<?= htmlspecialchars($currentView) ?>">
|
||||
<div class="pf-container cl-view-dynamic">
|
||||
|
||||
<div class="cl-titlebar">
|
||||
<h1>🎁 <?= sprintf(tr('gift_main_title'), $year) ?></h1>
|
||||
<div class="cl-view-switch">
|
||||
<a href="?view=nadal" class="cl-view-btn <?= $currentView === 'nadal' ? 'is-active' : '' ?>"><?= tr('gift_view_nadal') ?></a>
|
||||
<a href="?view=anniversary" class="cl-view-btn <?= $currentView === 'anniversary' ? 'is-active' : '' ?>"><?= tr('gift_view_anniv') ?></a>
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<h1 style="margin: 0;">🎁 <?= sprintf(tr('gift_main_title'), $year) ?></h1>
|
||||
<button id="btn-open-gift-settings" class="btn-settings-gear" title="<?= tr('gift_settings') ?>" style="font-size: 1.4rem; padding: 4px;">
|
||||
⚙️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pf-filter-bar">
|
||||
<span style="font-size:1.2rem;">🔍</span>
|
||||
<span class="pf-filter-icon">🔍</span>
|
||||
|
||||
<div class="pf-multi-select" id="ms-child">
|
||||
<div class="pf-ms-trigger" onclick="toggleMS('ms-child-list', this)">
|
||||
@@ -139,16 +129,20 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
|
||||
<section class="pf-section">
|
||||
<?php foreach ($allowedOccasions as $occCode): ?>
|
||||
<?php foreach ($activeOccasions as $occ):
|
||||
$occCode = $occ['code'];
|
||||
// Affichage Brut de la BDD pour le nom de l'occasion
|
||||
$occName = $occ['name'];
|
||||
$occIcon = $occ['icon'] ?: '🎁';
|
||||
?>
|
||||
<div class="js-occ-section">
|
||||
<h2 class="cl-occasion-title">
|
||||
<?php if(!empty($occasionIcons[$occCode])): ?><img src="<?= $occasionIcons[$occCode] ?>" class="cl-occasion-icon"><?php endif; ?>
|
||||
<?= $allOccasionLabels[$occCode] ?>
|
||||
<span class="cl-occasion-icon"><?= htmlspecialchars($occIcon) ?></span>
|
||||
<?= htmlspecialchars($occName) ?>
|
||||
</h2>
|
||||
|
||||
<?php foreach ($children as $child):
|
||||
$childData = $data[$occCode][$child] ?? ['gifts' => [], 'totals' => []];
|
||||
$adultsForThisChild = ($currentView === 'anniversary' && in_array($child, ['Pol', 'Pep'])) ? array_merge($baseAdults, $extraAdults) : $baseAdults;
|
||||
?>
|
||||
<div class="pf-child-section js-child" data-name="<?= htmlspecialchars($child) ?>">
|
||||
<div class="pf-child-header">
|
||||
@@ -156,7 +150,7 @@ require __DIR__ . '/header.php';
|
||||
<button class="pf-btn pf-btn-small btn-add-gift"
|
||||
data-child="<?= htmlspecialchars($child) ?>"
|
||||
data-occ="<?= htmlspecialchars($occCode) ?>"
|
||||
data-adults="<?= htmlspecialchars(json_encode(array_values($adultsForThisChild))) ?>">
|
||||
data-adults="<?= htmlspecialchars(json_encode(array_values($allAdultsList))) ?>">
|
||||
+ <?= tr('gift_add_gift') ?>
|
||||
</button>
|
||||
</div>
|
||||
@@ -168,9 +162,9 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
|
||||
<?php if (empty($childData['gifts'])): ?>
|
||||
<p class="js-empty-state" style="color:var(--text-muted); font-size:0.9rem; font-style:italic; margin:0;"><?= tr('gift_empty_state_no_gifts') ?></p>
|
||||
<p class="js-empty-state gift-empty-state"><?= tr('gift_empty_state_no_gifts') ?></p>
|
||||
<?php else: ?>
|
||||
<p class="js-empty-state" style="color:var(--text-muted); font-size:0.9rem; font-style:italic; display:none; margin:0;"><?= tr('gift_empty_state_no_filter') ?></p>
|
||||
<p class="js-empty-state gift-empty-state pf-hidden"><?= tr('gift_empty_state_no_filter') ?></p>
|
||||
<div class="pf-gift-feed">
|
||||
<?php foreach ($childData['gifts'] as $g): ?>
|
||||
<div class="pf-gift-card-compact js-gift-card" data-adult="<?= htmlspecialchars($g['adult_name']) ?>">
|
||||
@@ -201,41 +195,42 @@ require __DIR__ . '/header.php';
|
||||
<?php endforeach; ?>
|
||||
</section>
|
||||
|
||||
<section class="pf-section pf-section--panel" style="border:1px solid var(--border-light); margin-top:40px;">
|
||||
<h2 style="margin-top:0; color:var(--text-main); font-size:1.3rem;">⚖️ <?= tr('gift_liquidations') ?? 'Bilan & Remboursements' ?></h2>
|
||||
<section class="pf-section pf-section--panel gift-tricount-section">
|
||||
<h2 class="gift-tricount-title">⚖️ <?= tr('gift_liquidations') ?></h2>
|
||||
|
||||
<?php if (empty($settlements)): ?>
|
||||
<p style="color:var(--success); font-weight:700; margin-bottom:0;">✅ <?= tr('gift_no_debt') ?></p>
|
||||
<p class="gift-tricount-success">✅ <?= tr('gift_no_debt') ?></p>
|
||||
<?php else: ?>
|
||||
<ul class="pf-tricount-list">
|
||||
<?php foreach ($settlements as $s): ?>
|
||||
<li class="pf-tricount-item">
|
||||
<span><strong><?= htmlspecialchars($s['from']) ?></strong> <?= tr('gift_owes') ?> à <?= htmlspecialchars($s['to']) ?></span>
|
||||
<strong style="color:var(--danger);"><?= number_format($s['amount'], 2, ',', ' ') ?> €</strong>
|
||||
<span><strong><?= htmlspecialchars($s['from']) ?></strong> <?= tr('gift_owes') ?> <?= tr('gift_to') ?> <?= htmlspecialchars($s['to']) ?></span>
|
||||
<strong class="gift-tricount-debt-amount"><?= number_format($s['amount'], 2, ',', ' ') ?> €</strong>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
|
||||
<details style="margin-top:20px; padding:12px; border-radius:8px;">
|
||||
<summary style="cursor:pointer; font-weight:600; color:var(--text-muted); outline:none;"><?= tr('gift_view_matrix') ?></summary>
|
||||
<div style="overflow-x:auto; margin-top:15px;">
|
||||
<details class="gift-matrix-details">
|
||||
<summary class="gift-matrix-summary"><?= tr('gift_view_matrix') ?></summary>
|
||||
<div class="gift-matrix-wrapper">
|
||||
<table class="pf-table pf-table--compact cl-debt-matrix">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="position:sticky; left:0; background:#f8fafc; z-index:2; border-right:2px solid #e2e8f0;"><?= tr('gift_debtor') ?> \ <?= tr('gift_creditor') ?></th>
|
||||
<th class="sticky-col bg-header"><?= tr('gift_debtor') ?> \ <?= tr('gift_creditor') ?></th>
|
||||
<?php foreach ($people as $p): ?><th><?= htmlspecialchars($p) ?></th><?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($people as $debtor): ?>
|
||||
<tr>
|
||||
<th style="position:sticky; left:0; background:white; z-index:2; border-right:2px solid #e2e8f0;"><?= htmlspecialchars($debtor) ?></th>
|
||||
<th class="sticky-col bg-body"><?= htmlspecialchars($debtor) ?></th>
|
||||
<?php foreach ($people as $creditor):
|
||||
$val = $matrix[$debtor][$creditor] ?? 0;
|
||||
$display = ($debtor === $creditor) || $val == 0 ? '—' : number_format($val, 2, ',', ' ') . ' €';
|
||||
$cellClass = $val > 0 ? 'gift-cell-danger' : 'gift-cell-muted';
|
||||
?>
|
||||
<td style="<?= $val > 0 ? 'color:var(--danger); font-weight:700; background:#fef2f2;' : 'color:var(--text-muted);' ?>"><?= $display ?></td>
|
||||
<td class="<?= $cellClass ?>"><?= $display ?></td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
@@ -246,11 +241,12 @@ require __DIR__ . '/header.php';
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 1. Modale CRUD Cadeau -->
|
||||
<div id="pf-gift-modal" class="pf-modal">
|
||||
<div class="pf-modal-content" style="max-width: 500px; width: 95%;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||||
<h3 id="modalTitle" class="pf-modal-title" style="margin:0; border:none; padding:0;">Cadeau</h3>
|
||||
<button type="button" class="btn-modal-close" style="background:none; border:none; font-size:1.8rem; cursor:pointer; color:var(--text-muted); line-height:1;">×</button>
|
||||
<div class="pf-modal-content gift-modal-custom-content">
|
||||
<div class="gift-modal-custom-header">
|
||||
<h3 id="modalTitle" class="pf-modal-title gift-modal-custom-title">Cadeau</h3>
|
||||
<button type="button" class="btn-modal-close gift-modal-close-btn">×</button>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/modules/gift-list/save-gift.php" id="giftForm">
|
||||
@@ -264,9 +260,9 @@ require __DIR__ . '/header.php';
|
||||
<div class="pf-form-group"><label class="pf-label"><?= tr('gift_modal_payer') ?></label><select name="payer_name" id="modalPayer" class="pf-input" required></select></div>
|
||||
<div class="pf-form-group"><label class="pf-label"><?= tr('gift_modal_gift_name') ?></label><input type="text" name="gift_description" id="modalDesc" class="pf-input" required></div>
|
||||
<div class="pf-form-group"><label class="pf-label"><?= tr('gift_modal_price') ?></label><input type="number" step="0.01" name="amount" id="modalAmount" class="pf-input"></div>
|
||||
<div class="pf-form-group" style="margin-bottom:25px;"><label class="pf-label"><?= tr('gift_modal_link') ?></label><input type="url" name="product_link" id="modalLink" class="pf-input"></div>
|
||||
<div class="pf-form-group gift-form-group-spaced"><label class="pf-label"><?= tr('gift_modal_link') ?></label><input type="url" name="product_link" id="modalLink" class="pf-input"></div>
|
||||
|
||||
<div class="modal-footer" style="padding-top:15px; display:flex; justify-content:flex-end; gap:10px;">
|
||||
<div class="modal-footer gift-modal-custom-footer">
|
||||
<button type="button" class="pf-btn btn-secondary btn-modal-close"><?= tr('btn_cancel') ?></button>
|
||||
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
|
||||
</div>
|
||||
@@ -274,6 +270,91 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Modale Paramètres (Occasions & Proches) -->
|
||||
<div id="modal-gift-settings" class="pf-modal">
|
||||
<div class="pf-modal-content" style="max-width: 600px; width: 90%;">
|
||||
|
||||
<div class="pf-modal-header" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<h3 class="pf-modal-title" style="margin: 0;">⚙️ <?= tr('gift_settings') ?></h3>
|
||||
<button type="button" class="pf-modal-close" style="background:none; border:none; font-size:1.5rem; cursor:pointer;" onclick="closeGiftSettings()">×</button>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 4px; background: var(--bg-page); padding: 4px; border-radius: 8px; margin: 1rem 0;">
|
||||
<button type="button" id="tab-btn-occasions" class="bs-tab-btn active" style="flex: 1; padding: 8px 12px; font-weight: 600; border-radius: 6px; border: none; cursor: pointer; transition: 0.2s;" onclick="switchGLTab('occasions')"><?= tr('gift_tab_occasions') ?></button>
|
||||
<button type="button" id="tab-btn-proches" class="bs-tab-btn" style="flex: 1; padding: 8px 12px; font-weight: 600; border-radius: 6px; border: none; cursor: pointer; transition: 0.2s; background: transparent;" onclick="switchGLTab('proches')"><?= tr('gift_tab_proches') ?></button>
|
||||
</div>
|
||||
|
||||
<div class="pf-modal-body" style="min-height: 450px; max-height: 60vh; overflow-y: auto; padding: 0 1rem 1rem 1rem; display: flex; flex-direction: column;">
|
||||
|
||||
<div id="gl-pane-occasions" class="gl-pane active" style="display: block; animation: fadeIn 0.3s ease;">
|
||||
<div id="occasions-list" style="margin-bottom: 20px;">⏳ <?= tr('loading') ?></div>
|
||||
|
||||
<hr style="border:0; border-top: 1px solid var(--border-light); margin: 20px 0;">
|
||||
|
||||
<h5 style="margin: 0 0 15px 0; font-size: 1rem; color: var(--text-main);"><?= tr('gift_settings_add_occ') ?></h5>
|
||||
<form id="form-add-occasion" style="display: flex; flex-direction: column; gap: 16px;">
|
||||
<div>
|
||||
<label class="pf-label" style="display: block; margin-bottom: 6px;"><?= tr('gift_settings_icon') ?></label>
|
||||
<input type="hidden" name="icon" id="add-occ-icon" value="🎁">
|
||||
<div class="emoji-picker" style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||
<div class="emoji-option selected" onclick="selectEmoji(this, 'add-occ-icon', '🎁')">🎁</div>
|
||||
<div class="emoji-option" onclick="selectEmoji(this, 'add-occ-icon', '🎄')">🎄</div>
|
||||
<div class="emoji-option" onclick="selectEmoji(this, 'add-occ-icon', '🎂')">🎂</div>
|
||||
<div class="emoji-option" onclick="selectEmoji(this, 'add-occ-icon', '🎉')">🎉</div>
|
||||
<div class="emoji-option" onclick="selectEmoji(this, 'add-occ-icon', '🎓')">🎓</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
||||
<div style="flex: 1; min-width: 150px;">
|
||||
<label class="pf-label" style="display: block; margin-bottom: 6px;"><?= tr('gift_modal_gift_name') ?> *</label>
|
||||
<input type="text" name="name" id="add-occ-name" class="pf-input" style="width: 100%;" required>
|
||||
</div>
|
||||
<div style="width: 110px;">
|
||||
<label class="pf-label" style="display: block; margin-bottom: 6px;"><?= tr('gift_settings_date') ?></label>
|
||||
<input type="text" name="month_date" id="add-occ-date" class="pf-input" style="width: 100%;" placeholder="MM-DD">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 5px;">
|
||||
<button type="button" class="pf-btn btn-secondary" onclick="document.getElementById('form-add-occasion').reset()"><?= tr('btn_cancel') ?></button>
|
||||
<button type="submit" class="pf-btn pf-btn-primary"><?= tr('btn_add') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="gl-pane-proches" class="gl-pane" style="display: none; animation: fadeIn 0.3s ease;">
|
||||
<div id="proches-list" style="margin-bottom: 20px;">⏳ <?= tr('loading') ?></div>
|
||||
|
||||
<hr style="border:0; border-top: 1px solid var(--border-light); margin: auto 0 20px 0;">
|
||||
|
||||
<h5 style="margin: 0 0 15px 0; font-size: 1rem; color: var(--text-main);"><?= tr('gift_settings_add_proche') ?></h5>
|
||||
<form id="form-add-proche" style="display: flex; flex-direction: column; gap: 16px;">
|
||||
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
||||
<div style="flex: 1; min-width: 150px;">
|
||||
<label class="pf-label" style="display: block; margin-bottom: 6px;"><?= tr('gift_ph_proche_name') ?> *</label>
|
||||
<input type="text" name="name" id="add-proche-name" class="pf-input" style="width: 100%;" required>
|
||||
</div>
|
||||
<div style="flex: 1; min-width: 150px;">
|
||||
<label class="pf-label" style="display: block; margin-bottom: 6px;"><?= tr('gift_role_label') ?> *</label>
|
||||
<select name="role" id="add-proche-role" class="pf-input" style="width: 100%;" required>
|
||||
<option value="proche_adulte"><?= tr('gift_role_proche_adulte') ?></option>
|
||||
<option value="proche_enfant"><?= tr('gift_role_proche_enfant') ?></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 5px;">
|
||||
<button type="button" class="pf-btn btn-secondary" onclick="document.getElementById('form-add-proche').reset()"><?= tr('btn_cancel') ?></button>
|
||||
<button type="submit" class="pf-btn pf-btn-primary"><?= tr('btn_add') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ==========================================
|
||||
// 1. GESTION DU COMPOSANT MULTI-SELECT VANILLA
|
||||
@@ -381,14 +462,12 @@ function applyGiftFilters() {
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 3. LOGIQUE DE LA MODALE & DELEGATION JS
|
||||
// 2. GESTION DE LA MODALE CADEAU (AJOUT/ÉDITION)
|
||||
// ==========================================
|
||||
|
||||
const modal = document.getElementById('pf-gift-modal');
|
||||
const adultSelect = document.getElementById('modalAdult');
|
||||
const payerSelect = document.getElementById('modalPayer');
|
||||
|
||||
// Synchronisation automatique : Adulte -> Payeur
|
||||
if (adultSelect && payerSelect) {
|
||||
adultSelect.addEventListener('change', function() {
|
||||
let exists = false;
|
||||
@@ -409,10 +488,7 @@ function populateSelects(adults) {
|
||||
});
|
||||
}
|
||||
|
||||
// L'écouteur d'événements global pour tous les boutons !
|
||||
document.body.addEventListener('click', async function(e) {
|
||||
|
||||
// --- 1. BOUTON FERMER MODALE ---
|
||||
if (e.target.closest('.btn-modal-close')) {
|
||||
if (modal) {
|
||||
modal.classList.remove('open');
|
||||
@@ -421,7 +497,6 @@ document.body.addEventListener('click', async function(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// --- 2. BOUTON AJOUTER (+) ---
|
||||
const btnAdd = e.target.closest('.btn-add-gift');
|
||||
if (btnAdd) {
|
||||
const childName = btnAdd.dataset.child;
|
||||
@@ -453,7 +528,6 @@ document.body.addEventListener('click', async function(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// --- 3. BOUTON MODIFIER (CRAYON) ---
|
||||
const btnEdit = e.target.closest('.btn-edit-gift');
|
||||
if (btnEdit) {
|
||||
const data = JSON.parse(btnEdit.dataset.gift || '{}');
|
||||
@@ -485,7 +559,6 @@ document.body.addEventListener('click', async function(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// --- 4. BOUTON SUPPRIMER (POUBELLE) ---
|
||||
const btnDel = e.target.closest('.btn-delete-gift');
|
||||
if (btnDel) {
|
||||
const giftId = btnDel.dataset.id;
|
||||
@@ -506,10 +579,6 @@ document.body.addEventListener('click', async function(e) {
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// 4. SOUMISSION AJAX DU FORMULAIRE
|
||||
// ==========================================
|
||||
|
||||
const giftForm = document.getElementById('giftForm');
|
||||
if (giftForm) {
|
||||
giftForm.addEventListener('submit', async (e) => {
|
||||
@@ -529,6 +598,377 @@ if (giftForm) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 3. GESTION DES PARAMÈTRES (MODALE FÊTES & PROCHES)
|
||||
// ==========================================
|
||||
|
||||
function closeGiftSettings() {
|
||||
document.getElementById('modal-gift-settings').classList.remove('open');
|
||||
document.body.classList.remove('no-scroll');
|
||||
}
|
||||
|
||||
document.getElementById('btn-open-gift-settings')?.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
document.getElementById('modal-gift-settings').classList.add('open');
|
||||
document.body.classList.add('no-scroll');
|
||||
await loadSettingsData();
|
||||
});
|
||||
|
||||
document.querySelectorAll('.btn-close-settings').forEach(btn => btn.addEventListener('click', closeGiftSettings));
|
||||
|
||||
function switchGLTab(tabId) {
|
||||
// Gestion du contenu (Afficher/Cacher)
|
||||
document.getElementById('gl-pane-occasions').style.display = (tabId === 'occasions') ? 'block' : 'none';
|
||||
document.getElementById('gl-pane-proches').style.display = (tabId === 'proches') ? 'block' : 'none';
|
||||
|
||||
// Gestion du style des boutons d'onglet
|
||||
const btnOcc = document.getElementById('tab-btn-occasions');
|
||||
const btnProche = document.getElementById('tab-btn-proches');
|
||||
|
||||
if (tabId === 'occasions') {
|
||||
btnOcc.style.background = 'var(--bg-panel)';
|
||||
btnOcc.style.color = 'var(--primary)';
|
||||
btnOcc.style.boxShadow = 'var(--shadow-sm)';
|
||||
|
||||
btnProche.style.background = 'transparent';
|
||||
btnProche.style.color = 'var(--text-muted)';
|
||||
btnProche.style.boxShadow = 'none';
|
||||
} else {
|
||||
btnProche.style.background = 'var(--bg-panel)';
|
||||
btnProche.style.color = 'var(--primary)';
|
||||
btnProche.style.boxShadow = 'var(--shadow-sm)';
|
||||
|
||||
btnOcc.style.background = 'transparent';
|
||||
btnOcc.style.color = 'var(--text-muted)';
|
||||
btnOcc.style.boxShadow = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
let hasSettingsChanged = false;
|
||||
|
||||
async function loadSettingsData() {
|
||||
try {
|
||||
const res = await pachaFetch('/modules/gift-list/api-settings.php?action=get_all');
|
||||
if (!res.success) throw new Error(res.error);
|
||||
|
||||
renderOccasions(res.data.occasions);
|
||||
renderProches(res.data.proches);
|
||||
} catch (e) {
|
||||
document.getElementById('occasions-list').innerHTML = `<div class="pf-alert pf-alert--error">Erreur : ${e.message}</div>`;
|
||||
document.getElementById('proches-list').innerHTML = `<div class="pf-alert pf-alert--error">Erreur : ${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderOccasions(occasions) {
|
||||
const container = document.getElementById('occasions-list');
|
||||
if (!occasions.length) { container.innerHTML = `<em class="gift-empty-state">Vide</em>`; return; }
|
||||
|
||||
let html = '';
|
||||
occasions.forEach(occ => {
|
||||
const isChecked = occ.is_active == 1 ? 'checked' : '';
|
||||
// ⚠️ Remarque l'ajout de id="row-occ-${occ.id}" ici :
|
||||
html += `
|
||||
<div class="gl-list-item" id="row-occ-${occ.id}">
|
||||
<div>
|
||||
<span style="font-size:1.2rem; margin-right:8px;">${occ.icon || '🎁'}</span>
|
||||
<strong>${occ.name}</strong>
|
||||
<small style="color:var(--text-muted); margin-left:8px;">${occ.month_date || ''}</small>
|
||||
</div>
|
||||
<div class="pf-flex-gap-8 align-center">
|
||||
<label style="display:flex; align-items:center; cursor:pointer;">
|
||||
<input type="checkbox" class="pf-checkbox-lg" ${isChecked} onchange="toggleOccasion(${occ.id}, this.checked)">
|
||||
</label>
|
||||
<button type="button" class="btn-icon-action edit" onclick="transformRowToEditOccasion('row-occ-${occ.id}', ${occ.id}, '${occ.name.replace(/'/g, "\\'")}', '${occ.icon}', '${occ.month_date || ''}')" title="${tr('btn_edit')}">✏️</button>
|
||||
<button type="button" class="btn-icon-action delete" onclick="deleteOccasion(${occ.id})" title="${tr('btn_delete')}">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderProches(proches) {
|
||||
const container = document.getElementById('proches-list');
|
||||
if (!proches.length) { container.innerHTML = `<em class="gift-empty-state">Aucun proche configuré.</em>`; return; }
|
||||
|
||||
let html = '';
|
||||
proches.forEach(p => {
|
||||
const roleLabel = p.role === 'proche_adulte' ? (tr('gift_role_proche_adulte') || 'Adulte') : (tr('gift_role_proche_enfant') || 'Enfant');
|
||||
html += `
|
||||
<div class="gl-list-item" id="row-proche-${p.id}">
|
||||
<div><strong>${p.name}</strong> <small style="color:var(--text-muted);">— ${roleLabel}</small></div>
|
||||
<div class="pf-flex-gap-8 align-center">
|
||||
<button type="button" class="btn-icon-action edit" onclick="transformRowToEditProche('row-proche-${p.id}', ${p.id}, '${p.name.replace(/'/g, "\\'")}', '${p.role}')" title="${tr('btn_edit')}">✏️</button>
|
||||
<button type="button" class="btn-icon-action delete" onclick="deleteProche(${p.id})" title="${tr('btn_delete')}">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// CRUD : OCCASIONS
|
||||
// ==========================================
|
||||
|
||||
// 1. Ajouter une occasion
|
||||
document.getElementById('form-add-occasion')?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const btn = e.target.querySelector('button[type="submit"]');
|
||||
btn.disabled = true;
|
||||
hasSettingsChanged = true;
|
||||
|
||||
const fd = new FormData(e.target);
|
||||
fd.append('action', 'save_occasion');
|
||||
|
||||
try {
|
||||
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
|
||||
|
||||
// On vide le formulaire
|
||||
e.target.reset();
|
||||
|
||||
// On remet l'émoji par défaut visuellement
|
||||
document.querySelectorAll('#form-add-occasion .emoji-option').forEach(el => el.classList.remove('selected'));
|
||||
document.querySelector('#form-add-occasion .emoji-option').classList.add('selected');
|
||||
document.getElementById('add-occ-icon').value = '🎁';
|
||||
|
||||
showToast("Fête ajoutée avec succès !", "success");
|
||||
loadSettingsData(); // Recharge la liste
|
||||
} catch (err) {
|
||||
showToast("Erreur lors de l'ajout", "error");
|
||||
console.error(err);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Sauvegarder la modification (In-place)
|
||||
async function saveInlineOccasion(id) {
|
||||
hasSettingsChanged = true;
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'save_occasion');
|
||||
fd.append('id', id);
|
||||
fd.append('icon', document.getElementById(`inline-occ-icon-${id}`).value);
|
||||
fd.append('name', document.getElementById(`inline-occ-name-${id}`).value);
|
||||
fd.append('month_date', document.getElementById(`inline-occ-date-${id}`).value);
|
||||
|
||||
try {
|
||||
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
|
||||
showToast("Fête modifiée avec succès !", "success");
|
||||
loadSettingsData();
|
||||
} catch (err) {
|
||||
showToast("Erreur lors de la modification", "error");
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Supprimer une occasion
|
||||
async function deleteOccasion(id) {
|
||||
if (!confirm(tr('confirm_delete') || 'Supprimer définitivement ?')) return;
|
||||
hasSettingsChanged = true;
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'delete_occasion');
|
||||
fd.append('id', id);
|
||||
|
||||
try {
|
||||
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
|
||||
showToast("Fête supprimée.", "success");
|
||||
loadSettingsData();
|
||||
} catch (err) {
|
||||
showToast("Erreur lors de la suppression", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Activer / Désactiver (Checkbox)
|
||||
async function toggleOccasion(id, state) {
|
||||
hasSettingsChanged = true;
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'toggle_occasion');
|
||||
fd.append('id', id);
|
||||
fd.append('state', state ? 1 : 0);
|
||||
|
||||
try {
|
||||
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
|
||||
showToast(state ? "Fête activée" : "Fête masquée", "success");
|
||||
} catch (err) {
|
||||
showToast("Erreur de mise à jour", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// CRUD : PROCHES
|
||||
// ==========================================
|
||||
|
||||
// 1. Ajouter un proche
|
||||
document.getElementById('form-add-proche')?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const btn = e.target.querySelector('button[type="submit"]');
|
||||
btn.disabled = true;
|
||||
hasSettingsChanged = true;
|
||||
|
||||
const fd = new FormData(e.target);
|
||||
fd.append('action', 'save_proche');
|
||||
|
||||
try {
|
||||
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
|
||||
e.target.reset();
|
||||
showToast("Proche ajouté avec succès !", "success");
|
||||
loadSettingsData();
|
||||
} catch (err) {
|
||||
showToast("Erreur lors de l'ajout", "error");
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Sauvegarder la modification (In-place)
|
||||
async function saveInlineProche(id) {
|
||||
hasSettingsChanged = true;
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'save_proche');
|
||||
fd.append('id', id);
|
||||
fd.append('name', document.getElementById(`inline-proche-name-${id}`).value);
|
||||
fd.append('role', document.getElementById(`inline-proche-role-${id}`).value);
|
||||
|
||||
try {
|
||||
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
|
||||
showToast("Proche mis à jour !", "success");
|
||||
loadSettingsData();
|
||||
} catch (err) {
|
||||
showToast("Erreur lors de la mise à jour", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Supprimer un proche
|
||||
async function deleteProche(id) {
|
||||
if (!confirm(tr('confirm_delete') || 'Supprimer ce proche ?')) return;
|
||||
hasSettingsChanged = true;
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'delete_proche');
|
||||
fd.append('id', id);
|
||||
|
||||
try {
|
||||
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
|
||||
showToast("Proche retiré de la liste.", "success");
|
||||
loadSettingsData();
|
||||
} catch (err) {
|
||||
showToast("Erreur lors de la suppression", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Rechargement intelligent lors de la fermeture si modifications
|
||||
document.getElementById('modal-gift-settings').addEventListener('click', (e) => {
|
||||
if (e.target.id === 'modal-gift-settings' || e.target.classList.contains('btn-close-settings')) {
|
||||
if (hasSettingsChanged) {
|
||||
showToast(tr('gift_settings_saved') || 'Paramètres mis à jour !', 'success');
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
} else {
|
||||
closeGiftSettings();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Fonction pour gérer la sélection dans la grille d'émojis (formulaire d'ajout)
|
||||
function selectEmoji(element, targetInputId, emoji) {
|
||||
document.getElementById(targetInputId).value = emoji;
|
||||
// Mise à jour visuelle de la sélection
|
||||
const siblings = element.parentNode.querySelectorAll('.emoji-option');
|
||||
siblings.forEach(el => el.classList.remove('selected'));
|
||||
element.classList.add('selected');
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// LOGIQUE D'ÉDITION IN-PLACE (OCCASIONS)
|
||||
// ==========================================
|
||||
|
||||
// On crée une mémoire pour stocker le HTML d'origine de chaque ligne
|
||||
const rowStates = {};
|
||||
|
||||
function transformRowToEditOccasion(rowId, id, name, icon, date) {
|
||||
const row = document.getElementById(rowId);
|
||||
|
||||
// On sauvegarde l'état proprement en JS (fini les bugs d'affichage !)
|
||||
rowStates[rowId] = row.innerHTML;
|
||||
|
||||
row.innerHTML = `
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px; align-items: center; width: 100%; background: var(--bg-subtle); padding: 8px; border-radius: 8px; border: 1px solid var(--border-light);">
|
||||
<select id="inline-occ-icon-${id}" class="pf-input" style="width: 60px; padding: 6px; margin: 0;">
|
||||
<option value="🎁" ${icon==='🎁'?'selected':''}>🎁</option>
|
||||
<option value="🎄" ${icon==='🎄'?'selected':''}>🎄</option>
|
||||
<option value="🎂" ${icon==='🎂'?'selected':''}>🎂</option>
|
||||
<option value="🎉" ${icon==='🎉'?'selected':''}>🎉</option>
|
||||
<option value="🎓" ${icon==='🎓'?'selected':''}>🎓</option>
|
||||
</select>
|
||||
<input type="text" id="inline-occ-name-${id}" value="${name}" class="pf-input" style="flex: 1; min-width: 100px; padding: 6px; margin: 0;">
|
||||
<input type="text" id="inline-occ-date-${id}" value="${date}" class="pf-input" style="width: 70px; padding: 6px; margin: 0;" placeholder="MM-JJ">
|
||||
|
||||
<div style="display: flex; gap: 4px; margin-left: auto;">
|
||||
<button type="button" onclick="saveInlineOccasion(${id})" class="pf-btn pf-btn-primary" style="padding: 6px 10px;">💾</button>
|
||||
<button type="button" onclick="cancelInlineEdit('${rowId}')" class="pf-btn btn-secondary" style="padding: 6px 10px;">❌</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function saveInlineOccasion(id) {
|
||||
hasSettingsChanged = true;
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'save_occasion');
|
||||
fd.append('id', id);
|
||||
fd.append('icon', document.getElementById(`inline-occ-icon-${id}`).value);
|
||||
fd.append('name', document.getElementById(`inline-occ-name-${id}`).value);
|
||||
fd.append('month_date', document.getElementById(`inline-occ-date-${id}`).value);
|
||||
|
||||
// On met à jour en BDD puis on recharge la liste !
|
||||
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
|
||||
loadSettingsData();
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// LOGIQUE D'ÉDITION IN-PLACE (PROCHES)
|
||||
// ==========================================
|
||||
|
||||
function transformRowToEditProche(rowId, id, name, role) {
|
||||
const row = document.getElementById(rowId);
|
||||
rowStates[rowId] = row.innerHTML;
|
||||
|
||||
const roleAdulte = window.I18N ? window.I18N['gift_role_proche_adulte'] : 'Adulte';
|
||||
const roleEnfant = window.I18N ? window.I18N['gift_role_proche_enfant'] : 'Enfant';
|
||||
|
||||
row.innerHTML = `
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px; align-items: center; width: 100%; background: var(--bg-subtle); padding: 8px; border-radius: 8px; border: 1px solid var(--border-light);">
|
||||
<input type="text" id="inline-proche-name-${id}" value="${name}" class="pf-input" style="flex: 1; min-width: 100px; padding: 6px; margin: 0;">
|
||||
<select id="inline-proche-role-${id}" class="pf-input" style="width: 130px; padding: 6px; margin: 0;">
|
||||
<option value="proche_adulte" ${role==='proche_adulte'?'selected':''}>${roleAdulte}</option>
|
||||
<option value="proche_enfant" ${role==='proche_enfant'?'selected':''}>${roleEnfant}</option>
|
||||
</select>
|
||||
|
||||
<div style="display: flex; gap: 4px; margin-left: auto;">
|
||||
<button type="button" onclick="saveInlineProche(${id})" class="pf-btn pf-btn-primary" style="padding: 6px 10px;">💾</button>
|
||||
<button type="button" onclick="cancelInlineEdit('${rowId}')" class="pf-btn btn-secondary" style="padding: 6px 10px;">❌</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function saveInlineProche(id) {
|
||||
hasSettingsChanged = true;
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'save_proche');
|
||||
fd.append('id', id);
|
||||
fd.append('name', document.getElementById(`inline-proche-name-${id}`).value);
|
||||
fd.append('role', document.getElementById(`inline-proche-role-${id}`).value);
|
||||
|
||||
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
|
||||
loadSettingsData();
|
||||
}
|
||||
|
||||
// Fonction pour restaurer la ligne (sans casser le HTML)
|
||||
function cancelInlineEdit(rowId) {
|
||||
document.getElementById(rowId).innerHTML = rowStates[rowId];
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<?php require __DIR__ . '/footer.php'; ?>
|
||||
+333
-64
@@ -307,7 +307,6 @@ body.pf-home .pf-mobile-close-btn {
|
||||
.pf-muted-note {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.pf-muted-note.pf-muted-note--tight {
|
||||
margin-bottom: 8px;
|
||||
@@ -359,6 +358,58 @@ body.pf-home .pf-mobile-close-btn {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* --- SÉLECTEUR DE COULEUR THÉMATIQUE (SETTINGS) --- */
|
||||
.pf-color-picker-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.pf-input-color {
|
||||
/* On réduit la taille pour le rendre plus subtil */
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
/* On enlève les marges/paddings par défaut */
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
/* On le rend complètement rond, c'est plus moderne */
|
||||
border-radius: 50%;
|
||||
border: 2px solid #e2e8f0;
|
||||
/* On cache le fond par défaut du navigateur */
|
||||
background: transparent;
|
||||
box-shadow: var(--shadow-sm, 0 1px 2px rgba(0, 0, 0, 0.05));
|
||||
transition: all 0.2s ease;
|
||||
/* Optionnel : masquer la bordure interne sur Chrome/Safari */
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/* On s'assure que le carré de couleur interne (Webkit) remplit tout et est rond */
|
||||
.pf-input-color::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
.pf-input-color::-webkit-color-swatch {
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Même chose pour Firefox */
|
||||
.pf-input-color::-moz-color-swatch {
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.pf-input-color:hover {
|
||||
border-color: #94a3b8;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.pf-color-picker-group .pf-muted-note {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted, #64748b);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Tuile module (checkbox) — état coché via :has(), compatible thème */
|
||||
.pf-module-tile {
|
||||
display: flex;
|
||||
@@ -973,16 +1024,18 @@ p {
|
||||
--bg-page: #0d1117;
|
||||
--bg-panel: #161b22;
|
||||
--border-light: #30363d;
|
||||
--shadow: 0 4px 6px -1px rgba(0,0,0,.4), 0 2px 4px -2px rgba(0,0,0,.3);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,.5), 0 4px 6px -4px rgba(0,0,0,.4);
|
||||
--bg-subtle: rgba(255,255,255,.06);
|
||||
--bg-soft: rgba(59,130,246,.15);
|
||||
--bg-danger-soft: rgba(239,68,68,.1);
|
||||
--shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.3);
|
||||
--shadow-lg:
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -4px rgba(0, 0, 0, 0.4);
|
||||
--bg-subtle: rgba(255, 255, 255, 0.06);
|
||||
--bg-soft: rgba(59, 130, 246, 0.15);
|
||||
--bg-danger-soft: rgba(239, 68, 68, 0.1);
|
||||
--bg-overlay: rgba(0, 0, 0, 0.55);
|
||||
--text-danger-soft: #f87171;
|
||||
--border-strong: #484f58;
|
||||
--border-danger-soft: rgba(239,68,68,.3);
|
||||
--focus-ring: rgba(59,130,246,.25);
|
||||
--border-danger-soft: rgba(239, 68, 68, 0.3);
|
||||
--focus-ring: rgba(59, 130, 246, 0.25);
|
||||
--alert-success-bg: rgba(34, 197, 94, 0.12);
|
||||
--alert-success-border: rgba(34, 197, 94, 0.45);
|
||||
--alert-success-text: #86efac;
|
||||
@@ -993,67 +1046,283 @@ p {
|
||||
--badge-admin-bg: rgba(59, 130, 246, 0.2);
|
||||
--badge-admin-text: #93c5fd;
|
||||
}
|
||||
[data-theme="dark"] body { background: var(--bg-page); color: var(--text-main); }
|
||||
[data-theme="dark"] .pf-header { background: var(--bg-panel); border-color: var(--border-light); }
|
||||
[data-theme="dark"] .pf-logo { color: var(--primary); }
|
||||
[data-theme="dark"] .pf-nav-link { color: var(--text-muted); }
|
||||
[data-theme="dark"] .pf-nav-link:hover { background: rgba(255,255,255,.06); color: var(--text-main); }
|
||||
[data-theme="dark"] .pf-nav-link--active { background: rgba(59,130,246,.15); color: var(--primary); }
|
||||
[data-theme="dark"] .pf-user-badge { color: var(--text-muted); }
|
||||
[data-theme="dark"] .pf-logout-btn { color: #f87171; }
|
||||
[data-theme="dark"] .pf-logout-btn:hover { background: rgba(239,68,68,.1); border-color: rgba(239,68,68,.3); }
|
||||
[data-theme="dark"] .pf-burger-btn { color: var(--text-main); }
|
||||
[data-theme="dark"] .pf-mobile-menu { background: var(--bg-panel); color: var(--text-main); }
|
||||
[data-theme="dark"] .pf-mobile-menu-header { background: var(--bg-page); border-color: var(--border-light); }
|
||||
[data-theme="dark"] .pf-mobile-nav-link { color: var(--text-main); border-color: var(--border-light); }
|
||||
[data-theme="dark"] .pf-mobile-nav-link:active { background: rgba(255,255,255,.06); }
|
||||
[data-theme="dark"] .pf-module-card { background: var(--bg-panel); border-color: var(--border-light); }
|
||||
[data-theme="dark"] .pf-module-card:hover { border-color: #484f58; box-shadow: var(--shadow); }
|
||||
[data-theme="dark"] .pf-card-title { color: var(--text-main); }
|
||||
[data-theme="dark"] .pf-card-desc { color: var(--text-muted); }
|
||||
[data-theme="dark"] h1, [data-theme="dark"] h2, [data-theme="dark"] h3 { color: var(--text-main); }
|
||||
[data-theme="dark"] p { color: var(--text-muted); }
|
||||
[data-theme="dark"] .pf-btn.btn-secondary { background: var(--bg-panel); color: var(--text-muted); border-color: var(--border-light); }
|
||||
[data-theme="dark"] .pf-btn.btn-secondary:hover { background: rgba(255,255,255,.05); color: var(--text-main); border-color: #484f58; }
|
||||
[data-theme="dark"] .pf-input { background: #1c2128; border-color: var(--border-light); color: var(--text-main); }
|
||||
[data-theme="dark"] .pf-input::placeholder { color: #484f58; }
|
||||
[data-theme="dark"] .pf-input:focus { background: #1c2128; border-color: var(--primary); }
|
||||
[data-theme="dark"] .pf-table { background: var(--bg-panel); }
|
||||
[data-theme="dark"] .pf-table th { background: #1c2128; color: var(--text-muted); border-color: var(--border-light); }
|
||||
[data-theme="dark"] .pf-table td { border-color: rgba(48,54,61,.5); color: var(--text-main); }
|
||||
[data-theme="dark"] .pf-table tbody tr:hover { background: rgba(255,255,255,.03); }
|
||||
[data-theme="dark"] .pf-modal-content { background: var(--bg-panel); border: 1px solid var(--border-light); }
|
||||
[data-theme="dark"] .pf-modal-title { border-color: var(--border-light); color: var(--text-main); }
|
||||
[data-theme="dark"] .modal-footer { border-color: var(--border-light); }
|
||||
[data-theme="dark"] .pf-footer { background: var(--bg-panel); border-color: var(--border-light); }
|
||||
[data-theme="dark"] .pf-toast { background: var(--bg-panel); color: var(--text-main); box-shadow: 0 10px 25px rgba(0,0,0,.4); }
|
||||
[data-theme="dark"] .pf-login-card { background: var(--bg-panel); border-color: var(--border-light); }
|
||||
[data-theme="dark"] .pf-login-error { background: rgba(239,68,68,.1); border-color: rgba(239,68,68,.3); }
|
||||
[data-theme="dark"] .btn-icon-action.edit:hover { background: rgba(59,130,246,.15); }
|
||||
[data-theme="dark"] .btn-icon-action.delete:hover { background: rgba(239,68,68,.1); }
|
||||
[data-theme="dark"] body {
|
||||
background: var(--bg-page);
|
||||
color: var(--text-main);
|
||||
}
|
||||
[data-theme="dark"] .pf-header {
|
||||
background: var(--bg-panel);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
[data-theme="dark"] .pf-logo {
|
||||
color: var(--primary);
|
||||
}
|
||||
[data-theme="dark"] .pf-nav-link {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
[data-theme="dark"] .pf-nav-link:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: var(--text-main);
|
||||
}
|
||||
[data-theme="dark"] .pf-nav-link--active {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: var(--primary);
|
||||
}
|
||||
[data-theme="dark"] .pf-user-badge {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
[data-theme="dark"] .pf-logout-btn {
|
||||
color: #f87171;
|
||||
}
|
||||
[data-theme="dark"] .pf-logout-btn:hover {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
[data-theme="dark"] .pf-burger-btn {
|
||||
color: var(--text-main);
|
||||
}
|
||||
[data-theme="dark"] .pf-mobile-menu {
|
||||
background: var(--bg-panel);
|
||||
color: var(--text-main);
|
||||
}
|
||||
[data-theme="dark"] .pf-mobile-menu-header {
|
||||
background: var(--bg-page);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
[data-theme="dark"] .pf-mobile-nav-link {
|
||||
color: var(--text-main);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
[data-theme="dark"] .pf-mobile-nav-link:active {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
[data-theme="dark"] .pf-module-card {
|
||||
background: var(--bg-panel);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
[data-theme="dark"] .pf-module-card:hover {
|
||||
border-color: #484f58;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
[data-theme="dark"] .pf-card-title {
|
||||
color: var(--text-main);
|
||||
}
|
||||
[data-theme="dark"] .pf-card-desc {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
[data-theme="dark"] h1,
|
||||
[data-theme="dark"] h2,
|
||||
[data-theme="dark"] h3 {
|
||||
color: var(--text-main);
|
||||
}
|
||||
[data-theme="dark"] p {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
[data-theme="dark"] .pf-btn.btn-secondary {
|
||||
background: var(--bg-panel);
|
||||
color: var(--text-muted);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
[data-theme="dark"] .pf-btn.btn-secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: var(--text-main);
|
||||
border-color: #484f58;
|
||||
}
|
||||
[data-theme="dark"] .pf-input {
|
||||
background: #1c2128;
|
||||
border-color: var(--border-light);
|
||||
color: var(--text-main);
|
||||
}
|
||||
[data-theme="dark"] .pf-input::placeholder {
|
||||
color: #484f58;
|
||||
}
|
||||
[data-theme="dark"] .pf-input:focus {
|
||||
background: #1c2128;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
[data-theme="dark"] .pf-table {
|
||||
background: var(--bg-panel);
|
||||
}
|
||||
[data-theme="dark"] .pf-table th {
|
||||
background: #1c2128;
|
||||
color: var(--text-muted);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
[data-theme="dark"] .pf-table td {
|
||||
border-color: rgba(48, 54, 61, 0.5);
|
||||
color: var(--text-main);
|
||||
}
|
||||
[data-theme="dark"] .pf-table tbody tr:hover {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
[data-theme="dark"] .pf-modal-content {
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
[data-theme="dark"] .pf-modal-title {
|
||||
border-color: var(--border-light);
|
||||
color: var(--text-main);
|
||||
}
|
||||
[data-theme="dark"] .modal-footer {
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
[data-theme="dark"] .pf-footer {
|
||||
background: var(--bg-panel);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
[data-theme="dark"] .pf-toast {
|
||||
background: var(--bg-panel);
|
||||
color: var(--text-main);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
[data-theme="dark"] .pf-login-card {
|
||||
background: var(--bg-panel);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
[data-theme="dark"] .pf-login-error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
[data-theme="dark"] .btn-icon-action.edit:hover {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
[data-theme="dark"] .btn-icon-action.delete:hover {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
/* ─── Pill toggle thème ────────────────────────────────────── */
|
||||
.theme-switch {
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
background: none; border: none; cursor: pointer;
|
||||
padding: 3px 5px; border-radius: 8px; transition: background .15s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 3px 5px;
|
||||
border-radius: 8px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.theme-switch:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
[data-theme="dark"] .theme-switch:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.ts-sun,
|
||||
.ts-moon {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1;
|
||||
transition: opacity 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
[data-theme="dark"] .ts-sun {
|
||||
opacity: 0.3;
|
||||
}
|
||||
.ts-moon {
|
||||
opacity: 0.3;
|
||||
}
|
||||
[data-theme="dark"] .ts-moon {
|
||||
opacity: 1;
|
||||
}
|
||||
.theme-switch:hover { background: #f1f5f9; }
|
||||
[data-theme="dark"] .theme-switch:hover { background: rgba(255,255,255,.08); }
|
||||
.ts-sun, .ts-moon { font-size: .85rem; line-height: 1; transition: opacity .2s; user-select: none; }
|
||||
[data-theme="dark"] .ts-sun { opacity: .3; }
|
||||
.ts-moon { opacity: .3; }
|
||||
[data-theme="dark"] .ts-moon { opacity: 1; }
|
||||
.ts-track {
|
||||
position: relative; width: 36px; height: 20px;
|
||||
background: #cbd5e1; border-radius: 999px;
|
||||
flex-shrink: 0; transition: background .25s;
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
background: #cbd5e1;
|
||||
border-radius: 999px;
|
||||
flex-shrink: 0;
|
||||
transition: background 0.25s;
|
||||
}
|
||||
[data-theme="dark"] .ts-track {
|
||||
background: #2563eb;
|
||||
}
|
||||
[data-theme="dark"] .ts-track { background: #2563eb; }
|
||||
.ts-thumb {
|
||||
position: absolute; top: 3px; left: 3px;
|
||||
width: 14px; height: 14px; border-radius: 50%;
|
||||
background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.3);
|
||||
transition: transform .25s cubic-bezier(.4,0,.2,1);
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
[data-theme="dark"] .ts-thumb {
|
||||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
/* === ACCORDÉONS NATIFS (DETAILS/SUMMARY) === */
|
||||
.pf-accordion {
|
||||
background: var(--bg-page);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.pf-accordion-summary {
|
||||
padding: 14px 16px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
list-style: none; /* Masque la flèche native Firefox/Chrome */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--bg-page);
|
||||
transition:
|
||||
background 0.2s,
|
||||
color 0.2s;
|
||||
}
|
||||
.pf-accordion-summary::-webkit-details-marker {
|
||||
display: none; /* Masque la flèche native Safari */
|
||||
}
|
||||
.pf-accordion-summary:hover {
|
||||
background: var(--bg-subtle);
|
||||
color: var(--text-main);
|
||||
}
|
||||
.pf-accordion-summary::after {
|
||||
content: "▼";
|
||||
font-size: 0.7rem;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
details[open].pf-accordion > .pf-accordion-summary {
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
color: var(--primary);
|
||||
}
|
||||
details[open].pf-accordion > .pf-accordion-summary::after {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
.pf-accordion-content {
|
||||
padding: 16px;
|
||||
background: var(--bg-panel);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
/* Bouton Paramètres avec icône rotative (Roue crantée) */
|
||||
.btn-settings-gear {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
font-size: 1.4rem;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.3s ease;
|
||||
line-height: 1;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.btn-settings-gear:hover {
|
||||
transform: rotate(
|
||||
90deg
|
||||
); /* 90deg rend mieux sur une roue crantée qu'un 45deg ! */
|
||||
}
|
||||
[data-theme="dark"] .ts-thumb { transform: translateX(16px); }
|
||||
|
||||
+5
-3
@@ -130,10 +130,12 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
||||
*/
|
||||
window.I18N = <?php echo json_encode($current_translations_array ?? [], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE); ?>;
|
||||
|
||||
// ✅ Injection sécurisée (évite le crash si config.php n'est pas chargé)
|
||||
// ✅ Injection sécurisée
|
||||
window.CONFIG = {
|
||||
ID_ALEX: <?php echo defined('ID_ALEX') ? ID_ALEX : 2; ?>,
|
||||
ID_LAIA: <?php echo defined('ID_LAIA') ? ID_LAIA : 3; ?>,
|
||||
// Liste complète des profils de la famille (Membres, Nounou, Enfants)
|
||||
PEOPLE: <?= isset($familyPeople) ? json_encode($familyPeople) : '[]' ?>,
|
||||
// ID du user global connecté (pour savoir "qui" clique)
|
||||
CURRENT_USER_ID: <?= isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : 'null' ?>,
|
||||
CURRENCY: '<?php echo defined('CURRENCY') ? CURRENCY : "€"; ?>',
|
||||
ZONE_SCOLAIRE: '<?php echo defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : "C"; ?>'
|
||||
};
|
||||
|
||||
+16
-2
@@ -22,6 +22,9 @@ $pageCss = "/modules/holidays/holidays.css";
|
||||
|
||||
require __DIR__ . '/header.php';
|
||||
|
||||
$stmtVehicles = $pdo->query("SELECT id, name FROM pf_vehicles ORDER BY name ASC");
|
||||
$garageVehicles = $stmtVehicles->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 3. ROUTEUR DU MODULE VACANCES
|
||||
if ($tab === 'holiday_detail' && isset($_GET['id'])) {
|
||||
// Si on demande le détail ET qu'un ID est fourni
|
||||
@@ -31,7 +34,18 @@ if ($tab === 'holiday_detail' && isset($_GET['id'])) {
|
||||
require __DIR__ . '/modules/holidays/views/list.php';
|
||||
}
|
||||
|
||||
// 4. Inclusion du JS global du module (Pont i18n déjà géré dans le header)
|
||||
echo '<script src="/modules/holidays/holidays.js"></script>';
|
||||
// 4. Inclusion des librairies globales du module (Flatpickr)
|
||||
?>
|
||||
<!-- Flatpickr (Vanilla JS Date Range) -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/themes/dark.css" id="flatpickr-dark-theme" disabled>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/fr.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/cat.js"></script>
|
||||
|
||||
<?php
|
||||
$jsPath = __DIR__ . '/modules/holidays/holidays.js';
|
||||
$jsVersion = file_exists($jsPath) ? filemtime($jsPath) : time();
|
||||
echo '<script src="/modules/holidays/holidays.js?v=' . $jsVersion . '"></script>';
|
||||
|
||||
require __DIR__ . '/footer.php';
|
||||
@@ -54,3 +54,8 @@ function verify_csrf(?string $token): bool
|
||||
}
|
||||
return hash_equals($_SESSION['csrf_token'], $token);
|
||||
}
|
||||
if (!isset($familyPeople) && !empty($family_id)) {
|
||||
require_once __DIR__ . '/db.php';
|
||||
$stmtPeople = $pdo->query("SELECT id, name, user_id FROM pf_people ORDER BY id ASC");
|
||||
$familyPeople = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<?php
|
||||
// includes/config.php
|
||||
// HouseHub Config - Version dynamique
|
||||
if (!defined('ID_ALEX')) {
|
||||
define('ID_ALEX', 2);
|
||||
define('ID_LAIA', 3);
|
||||
}
|
||||
+35
-32
@@ -1,56 +1,59 @@
|
||||
<?php
|
||||
// includes/db.php
|
||||
|
||||
if (file_exists(__DIR__ . '/config.php')) {
|
||||
require_once __DIR__ . '/config.php';
|
||||
}
|
||||
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
$host = getenv('DB_HOST') ?: 'househub-db';
|
||||
$envHost = getenv('DB_HOST');
|
||||
|
||||
// 1. Détection stricte de l'environnement
|
||||
if ($envHost === 'househub-db') {
|
||||
// Docker
|
||||
$host = 'househub-db';
|
||||
$user = getenv('DB_USER') ?: 'househub';
|
||||
$pass = getenv('DB_PASS') ?: 'changeme';
|
||||
$db = $_SESSION['family_db'] ?? null;
|
||||
|
||||
if (!$db) {
|
||||
$current = basename($_SERVER['PHP_SELF'] ?? '');
|
||||
if (!in_array($current, ['login.php', 'register.php'])) {
|
||||
header('Location: /login.php');
|
||||
exit;
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
// XAMPP
|
||||
$host = '127.0.0.1'; // Plus stable que 'localhost' sous XAMPP
|
||||
$user = 'root';
|
||||
$pass = '';
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = new PDO(
|
||||
"mysql:host=$host;dbname=$db;charset=utf8mb4",
|
||||
$user, $pass,
|
||||
[
|
||||
// 2. Multi-tenant strict : cibler la DB de la famille courante
|
||||
$db = 'househub_meta'; // Fallback par défaut
|
||||
if (!empty($_SESSION['user']['family_id'])) {
|
||||
$db = 'househub_f' . (int)$_SESSION['user']['family_id'];
|
||||
}
|
||||
|
||||
$charset = 'utf8mb4';
|
||||
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
|
||||
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_general_ci",
|
||||
PDO::ATTR_TIMEOUT => 30,
|
||||
]
|
||||
);
|
||||
$pdo->exec("SET collation_connection = utf8mb4_general_ci");
|
||||
];
|
||||
|
||||
try {
|
||||
$foyer = $pdo->query("SELECT currency, zone_scolaire FROM pf_foyer_settings WHERE id = 1")->fetch();
|
||||
if (!defined('CURRENCY')) {
|
||||
define('CURRENCY', $foyer['currency'] ?? '€');
|
||||
}
|
||||
if (!defined('ZONE_SCOLAIRE')) {
|
||||
define('ZONE_SCOLAIRE', $foyer['zone_scolaire'] ?? 'C');
|
||||
}
|
||||
} catch (\PDOException $e) {
|
||||
if (!defined('CURRENCY')) define('CURRENCY', '€');
|
||||
if (!defined('ZONE_SCOLAIRE')) define('ZONE_SCOLAIRE', 'C');
|
||||
}
|
||||
// ------------------------------------------------
|
||||
$pdo = new PDO($dsn, $user, $pass, $options);
|
||||
|
||||
// Forcer la collation
|
||||
$pdo->exec("SET collation_connection = utf8mb4_general_ci");
|
||||
$pdo->exec("SET collation_database = utf8mb4_general_ci");
|
||||
$pdo->exec("SET collation_server = utf8mb4_general_ci");
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
if (!headers_sent()) { header('Content-Type: application/json'); http_response_code(500); }
|
||||
die(json_encode(['ok' => false, 'error' => 'Erreur BDD : ' . $e->getMessage()]));
|
||||
die("Erreur de connexion BDD (Host: $host, DB: $db) : " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
+359
-38
@@ -156,6 +156,20 @@ return [
|
||||
'mod_planka_desc' => 'Gestioneu els vostres projectes en mode Kanban amb Planka.',
|
||||
'menu_planka' => 'Planka',
|
||||
|
||||
// --- SETTINGS : ENFANTS ---
|
||||
'set_kids_title' => 'Nens de la llar',
|
||||
'set_kids_desc' => 'Gestioneu la llista de nens per associar-los a l\'estalvi, als regals, etc.',
|
||||
'set_add_kid_placeholder' => 'Nom del nen',
|
||||
'set_btn_add_kid' => 'Afegir',
|
||||
'set_err_kid_name' => 'El nom és obligatori.',
|
||||
'set_confirm_del_kid' => 'Vols eliminar aquest nen? (El seu historial financer es conservarà als arxius).',
|
||||
'set_no_kids' => 'Cap nen configurat per a aquesta llar.',
|
||||
'set_family_config' => 'Configuració de la llar',
|
||||
'set_global_params' => 'Paràmetres globals',
|
||||
'set_care_modes' => 'Modalitats de cura habituals',
|
||||
'set_add_care_mode' => 'Afegir una modalitat',
|
||||
'btn_save' => 'Desar',
|
||||
|
||||
// ==========================================
|
||||
// FAMILY CALENDAR
|
||||
// ==========================================
|
||||
@@ -199,6 +213,122 @@ return [
|
||||
'fc_err_action' => 'Error en l\'acció: ',
|
||||
'fc_today_short' => 'Avui',
|
||||
'fc_today_title' => 'Anar a avui',
|
||||
'fc_tab_foyer' => '🏡 Llar',
|
||||
'fc_tab_members' => '👥 Membres i Drets',
|
||||
'fc_select_member' => 'Selecciona un membre de la família:',
|
||||
'fc_role_adult' => 'Adult',
|
||||
'fc_role_child' => 'Nen',
|
||||
'fc_care_modes_title' => '👶 Modes de cura associats',
|
||||
'fc_care_modes_desc' => 'Marca els modes de cura que s\'apliquen a aquest nen.',
|
||||
'fc_no_care_mode' => 'Cap mode configurat.',
|
||||
'fc_matrix_title' => '👨👩👦 Matriu de Drets de Permís',
|
||||
'fc_matrix_desc' => 'Declara els codis de comptadors i les dates de renovació.',
|
||||
'fc_matrix_code' => 'Codi',
|
||||
'fc_matrix_date' => 'Data de Reset',
|
||||
'fc_matrix_empty' => 'Cap comptador configurat.',
|
||||
'fc_matrix_saved' => 'Matriu de drets actualitzada! 💼',
|
||||
'fc_child_saved' => 'Subscripció del nen desada!',
|
||||
'btn_add_counter' => '➕ Afegir',
|
||||
'btn_save_rights' => '💾 Desar',
|
||||
'fc_modal_settings_title' => 'Configuració del Calendari',
|
||||
'fc_zone_label' => 'Zona Escolar (Vacances)',
|
||||
'fc_zone_desc' => 'Determina el calendari automàtic de vacances escolars.',
|
||||
'fc_care_modes_label' => 'Modes de Cura de la Llar',
|
||||
'fc_care_modes_desc' => 'Gestiona la llista global de modes de cura possibles per a la família.',
|
||||
'fc_add_mode_placeholder' => 'Ex: Mainadera, Casal, Avis...',
|
||||
'fc_settings_updated' => 'Configuració actualitzada correctament!',
|
||||
'fc_first_day' => 'Primer dia de la setmana',
|
||||
// --- CALENDARI : PARÀMETRES (MEMBRES I VISUALITZACIÓ) ---
|
||||
'fc_tab_display' => '🖥️ Visualització',
|
||||
'fc_display_title' => 'Ajustos de visualització',
|
||||
'fc_display_desc' => 'Personalitza l\'aparença i el comportament predeterminat del teu calendari.',
|
||||
'fc_default_view' => 'Vista per defecte',
|
||||
'fc_view_month' => '1 mes',
|
||||
'fc_view_2months' => '2 mesos',
|
||||
'fc_view_3months' => '3 mesos',
|
||||
'fc_working_hours' => 'Horari de cura (Per defecte)',
|
||||
'fc_working_hours_desc' => 'Exemple: 08:30-18:00',
|
||||
|
||||
'fc_role_unknown' => 'Desconegut',
|
||||
'fc_role_adult' => 'Adult',
|
||||
'fc_role_child' => 'Nen/a',
|
||||
'fc_role_helper' => 'Ajudant',
|
||||
'fc_error' => 'Error: ',
|
||||
|
||||
'fc_care_modes_title' => 'Modes de Cura Predeterminats',
|
||||
'fc_care_modes_desc' => 'Quins modes de cura s\'apliquen a aquest nen/a?',
|
||||
'fc_no_care_mode' => 'No hi ha modes de cura configurats a la llar.',
|
||||
'fc_child_saved' => 'Desat!',
|
||||
|
||||
'fc_matrix_title' => 'Matriu de Permisos',
|
||||
'fc_matrix_desc' => 'Defineix les quotes i dates de renovació.',
|
||||
'fc_matrix_empty' => 'No hi ha comptadors definits.',
|
||||
'fc_matrix_saved' => 'Matriu desada!',
|
||||
|
||||
'fc_leave_obsolete' => 'Obsolet',
|
||||
'fc_leave_code_placeholder' => 'Codi...',
|
||||
'fc_leave_method_fixed' => 'Fix',
|
||||
'fc_leave_method_accumulated' => 'Acumulat',
|
||||
|
||||
'fc_date_format_placeholder' => 'DD/MM',
|
||||
'fc_date_format_title' => 'Format DD/MM',
|
||||
|
||||
'fc_table_code' => 'Codi',
|
||||
'fc_table_method' => 'Mètode',
|
||||
'fc_table_quota' => 'Quota',
|
||||
'fc_table_renewal' => 'Renov.',
|
||||
|
||||
'btn_save_rights' => 'Guardar',
|
||||
|
||||
// ==========================================
|
||||
// MODAL DE CONFIGURACIÓ DEL CALENDARI
|
||||
// ==========================================
|
||||
'fc_modal_settings_title' => 'Configuració del calendari',
|
||||
'fc_tab_foyer' => 'Llar',
|
||||
'fc_tab_members' => 'Membres i Drets',
|
||||
|
||||
// Llar: Zona escolar
|
||||
'fc_zone_label' => 'Zona escolar',
|
||||
'fc_zone_a' => 'Zona A',
|
||||
'fc_zone_b' => 'Zona B',
|
||||
'fc_zone_c' => 'Zona C',
|
||||
'fc_zone_other' => 'Altra / Fora de França',
|
||||
'fc_zone_desc' => 'Permet mostrar correctament les vacances escolars.',
|
||||
|
||||
// Llar: Opcions de cura (Modes de garde)
|
||||
'fc_care_modes_label' => 'Modalitats de cura',
|
||||
'fc_add_mode_placeholder' => 'Ex: Mainadera, Llar d\'infants...',
|
||||
'fc_care_modes_desc' => 'Afegeix les diferents modalitats de cura per als teus fills.',
|
||||
'fc_empty_kids_msg' => 'Actualment no hi ha cap infant configurat a la teva família.',
|
||||
'fc_link_settings' => 'Anar a la configuració general',
|
||||
|
||||
// Llar: Catàleg de permisos (Congés)
|
||||
'fc_leave_catalog_title' => 'Catàleg de permisos',
|
||||
'fc_leave_catalog_desc' => 'Defineix els tipus de permisos possibles per a tota la família.',
|
||||
'fc_add_leave_type' => 'Afegir un tipus de permís',
|
||||
'fc_col_code' => 'Codi',
|
||||
'fc_code_irreversible' => 'Irreversible',
|
||||
'fc_col_label' => 'Etiqueta',
|
||||
'fc_leave_label_ph' => 'Ex: Vacances, Assumptes propis...',
|
||||
'fc_quota_default' => 'Quota per defecte',
|
||||
'fc_reset_month' => 'Mes de renovació',
|
||||
|
||||
// Membres
|
||||
'fc_select_member' => 'Selecciona un membre:',
|
||||
|
||||
// Mesos
|
||||
'month_jan' => 'Gener',
|
||||
'month_feb' => 'Febrer',
|
||||
'month_mar' => 'Març',
|
||||
'month_apr' => 'Abril',
|
||||
'month_may' => 'Maig',
|
||||
'month_jun' => 'Juny',
|
||||
'month_jul' => 'Juliol',
|
||||
'month_aug' => 'Agost',
|
||||
'month_sep' => 'Setembre',
|
||||
'month_oct' => 'Octubre',
|
||||
'month_nov' => 'Novembre',
|
||||
'month_dec' => 'Desembre',
|
||||
|
||||
'leg_presence' => 'Presència Pep',
|
||||
'leg_school_holidays' => 'Vacances Escolars',
|
||||
@@ -208,6 +338,9 @@ return [
|
||||
'leg_centre' => 'Casal',
|
||||
'leg_avis' => 'Avís',
|
||||
'leg_pep_sick' => 'Pep Malalt',
|
||||
'care_nounou' => 'Kangur',
|
||||
'care_centre' => 'Casal',
|
||||
'care_avis' => 'Avis',
|
||||
|
||||
'vac_toussaint' => 'Vacances de Tots Sants',
|
||||
'vac_noel' => 'Vacances de Nadal',
|
||||
@@ -385,6 +518,71 @@ return [
|
||||
'cat_fixed' => 'Despeses Fixes',
|
||||
'cat_others' => 'Altres / Imprevistos',
|
||||
'cat_savings' => 'Estalvis',
|
||||
'budget_tab_kids' => 'Nens',
|
||||
'budget_settings_title' => 'Configuració del Pressupost',
|
||||
'bs_tab_accounts' => '💳 Comptes',
|
||||
'bs_tab_categories' => '🏷️ Categories',
|
||||
'bs_tab_rules' => '🤖 Regles d\'importació',
|
||||
'bs_tab_salaries' => '💼 Salaris',
|
||||
'bs_empty_accounts' => 'Cap compte configurat.',
|
||||
'bs_type_checking' => 'Corrent',
|
||||
'bs_type_savings' => 'Estalvi',
|
||||
'bs_badge_default' => 'Per defecte',
|
||||
'bs_owner' => 'Propietari',
|
||||
'bs_shared_account' => 'Compte conjunt',
|
||||
'bs_empty_categories' => 'Cap categoria configurada.',
|
||||
'bs_type_income' => 'Ingrés',
|
||||
'bs_type_expense' => 'Despesa',
|
||||
'bs_type_savings_cat' => 'Estalvi',
|
||||
'bs_empty_rules' => 'Cap regla d\'importació.',
|
||||
'bs_empty_salaries' => 'Cap salari definit per a l\'any',
|
||||
'bs_salaries_year_desc' => 'Configuracions per a l\'any',
|
||||
'bs_salary_net' => 'Salari net',
|
||||
'bs_salary_fees' => 'Mensualitat / Despeses',
|
||||
'bs_add_account_title' => 'Afegir un compte',
|
||||
'bs_account_name_ph' => 'Nom (ex: Estalvi Bebè)',
|
||||
'bs_confirm_delete' => 'Segur que voleu suprimir aquest compte?',
|
||||
'bs_add_category_title' => '+ Afegir una categoria',
|
||||
'bs_cat_code_ph' => 'Codi (ex: FOOD)',
|
||||
'bs_cat_label_ph' => 'Etiqueta (ex: Compres)',
|
||||
'bs_cat_icon_ph' => 'Icona (ex: 🛒)',
|
||||
'bs_confirm_delete_cat' => 'Segur que voleu suprimir aquesta categoria?',
|
||||
'bs_add_rule_title' => 'Afegir una regla',
|
||||
'bs_rule_keyword_ph' => 'Paraula clau (ex: AUCHAN)',
|
||||
'bs_confirm_delete_rule'=> 'Segur que voleu suprimir aquesta regla?',
|
||||
'bs_currency_label' => 'Divisa de la llar',
|
||||
'bs_currency_desc' => 'Divisa principal utilitzada per mostrar els vostres comptes i pressupostos.',
|
||||
'bs_currency_updated' => 'Divisa actualitzada correctament!',
|
||||
// ONGLET CSV MAPPING
|
||||
'bs_csv_title' => 'Format de la importació CSV',
|
||||
'bs_csv_desc' => 'Configureu aquí les columnes que corresponen a l\'exportació del vostre banc.',
|
||||
'bs_csv_delimiter' => 'Separador',
|
||||
'bs_csv_delim_semi' => 'Punt i coma (;)',
|
||||
'bs_csv_delim_comma' => 'Coma (,)',
|
||||
'bs_csv_delim_tab' => 'Tabulació',
|
||||
'bs_csv_date_format' => 'Format de la Data',
|
||||
'bs_csv_col_index' => 'Índex de les columnes',
|
||||
'bs_csv_col_index_help' => '(Atenció: la 1a columna és la Número 0)',
|
||||
'bs_csv_col_date' => 'Núm Col: Data',
|
||||
'bs_csv_col_label' => 'Núm Col: Concepte / Motiu',
|
||||
'bs_csv_amount_mgmt' => 'Gestió dels imports',
|
||||
'bs_csv_amount_single' => 'Una sola columna (Positiu = Ingrés, Negatiu = Despesa)',
|
||||
'bs_csv_amount_split' => 'Dues columnes separades (Dèbit / Crèdit)',
|
||||
'bs_csv_col_amount' => 'Núm Col: Import',
|
||||
'bs_csv_col_debit' => 'Núm Col: Dèbit',
|
||||
'bs_csv_col_credit' => 'Núm Col: Crèdit',
|
||||
'bs_csv_col_ref' => 'Núm Col: Referència única (Opcional)',
|
||||
'btn_save_format' => 'Desar el format',
|
||||
'bs_csv_saved' => 'Format CSV desat',
|
||||
'bs_tab_csv' => '📥 Importació CSV',
|
||||
'bs_csv_settings_title' => 'Configuració del format',
|
||||
'bs_csv_drop_title' => 'Arrossegueu el vostre fitxer CSV aquí',
|
||||
'bs_csv_drop_desc' => 'o feu clic per cercar fitxers',
|
||||
'bs_csv_err_type' => 'Error: Només s\'admeten fitxers .csv.',
|
||||
'bs_csv_file_ok' => 'Fitxer carregat',
|
||||
'bs_csv_preview_title' => 'Previsualització de les vostres columnes',
|
||||
'bs_csv_empty_col' => 'Buit',
|
||||
'bs_csv_no_data' => 'No s\'ha trobat cap línia de transacció en aquest fitxer.',
|
||||
|
||||
// --- BUDGET : SUIVI ---
|
||||
'bud_rem_school' => 'Escola (Resta estimada)',
|
||||
@@ -442,6 +640,26 @@ return [
|
||||
'bud_bar_actual' => "Actual",
|
||||
'bud_bar_theoretical' => "Teòric",
|
||||
'bud_bar_max_cap' => "Capacitat Màx.",
|
||||
'bud_edit_title' => 'Modificar la transacció',
|
||||
'bud_to_define_js' => 'per definir',
|
||||
'bud_select_beneficiary' => 'Triar un ingrés esperat',
|
||||
'bud_bar_actual' => 'Actual',
|
||||
|
||||
// ONBOARDING BUDGET
|
||||
'bs_onb_title' => 'Benvinguts! Configurem el teu pressupost:',
|
||||
'bs_onb_desc' => 'Completa aquests passos perquè l\'aplicació pugui calcular el teu pressupost disponible:',
|
||||
'bs_onb_cat_link' => 'Categories',
|
||||
'bs_onb_cat_desc' => 'Crea almenys una categoria de despeses.',
|
||||
'bs_onb_acc_link' => 'Comptes Bancaris',
|
||||
'bs_onb_acc_desc' => 'Afegeix almenys un compte corrent o d\'estalvi.',
|
||||
'bs_onb_sal_link' => 'Sous',
|
||||
'bs_onb_sal_desc' => 'Introdueix els ingressos de l\'any en curs.',
|
||||
'bs_onb_items_link' => 'Línies de Pressupost',
|
||||
'bs_onb_items_desc' => 'Ves a la pestanya Resum per llistar les teves despeses fixes (Lloguer, Assegurances...).',
|
||||
'bs_rule_search_ph' => '🔍 Cercar una paraula clau (ex: AUCHAN)...',
|
||||
'error_loading_settings' => 'Error en carregar la configuració',
|
||||
'settings' => 'Configuració',
|
||||
|
||||
// --- MODULE BUDGET : IMPORT CSV ---
|
||||
'bud_import_csv' => "Importar CSV",
|
||||
'bud_csv_file' => "Seleccioneu el fitxer del vostre banc (.csv)",
|
||||
@@ -499,6 +717,9 @@ return [
|
||||
'bud_sav_modal_title_add' => 'Introduir un mes',
|
||||
'bud_sav_ph_name' => 'Nom (ex: Vacances)',
|
||||
'bud_prev_label_name' => 'Nom',
|
||||
'bud_prev_monthly_target' => 'Import objectiu / Mensualitat (€)',
|
||||
'bud_prev_transfer_dest' => 'Destinació de la transferència',
|
||||
'bud_prev_none' => 'Cap destinació (Despesa estàndard)',
|
||||
|
||||
// --- BUDGET : EPARGNE ---
|
||||
'bud_sav_add_month' => 'Introduir un mes',
|
||||
@@ -529,6 +750,60 @@ return [
|
||||
'bud_sav_prompt_duplicate' => "Vols copiar les dades de %s a %t1?\n\nIntrodueix el nou TOTAL al banc (€) per a %t2:",
|
||||
'bud_err_server' => 'Error del servidor: ',
|
||||
'bud_err_network_dup' => 'Error de xarxa en duplicar.',
|
||||
'budget_tab_kids' => 'Infants 👶',
|
||||
'bud_sav_add_one_month' => '+1 Mes',
|
||||
'bud_sav_add_month' => 'Afegir un mes',
|
||||
'bud_sav_no_data' => 'Cap dada d\'estalvi per a %s.',
|
||||
'bud_sav_post_month' => 'Concepte / Mes',
|
||||
'bud_sav_from_date' => 'Des del %s',
|
||||
'bud_sav_edit_modal' => 'Editar aquest mes',
|
||||
'bud_sav_delete_month' => 'Suprimir aquest mes',
|
||||
'bud_sav_total_bank' => 'TOTAL BANC',
|
||||
'bud_sav_extra' => 'NO ASSIGNAT (EXTRES)',
|
||||
'bud_sav_modal_title_add' => 'Afegir un mes',
|
||||
'bud_sav_modal_title_edit' => 'Editar',
|
||||
'bud_sav_month_concerned' => 'Mes afectat',
|
||||
'bud_sav_total_bank_eur' => 'Total al Banc (€)',
|
||||
'bud_sav_ventilation' => 'Desglossament de comptes',
|
||||
'bud_sav_adj_help' => 'Utilitzeu l\'ajust (+/-) per recalcular automàticament.',
|
||||
'bud_sav_add_line' => 'Afegir una línia',
|
||||
'bud_category' => 'Categoria',
|
||||
'bud_sav_current' => 'Actual',
|
||||
'bud_sav_adjust' => 'Ajust',
|
||||
'bud_sav_new' => 'Nou',
|
||||
'bud_sav_sum_mode_title' => 'Activar/Desactivar la calculadora',
|
||||
'bud_sav_selection' => 'Selecció:',
|
||||
'bud_sav_ph_name' => 'Nom del compte',
|
||||
|
||||
|
||||
// --- Module Budget : Provisions & Optimisation ---
|
||||
'budget_provisions_title' => 'Provisions & Optimisation de Trésorerie',
|
||||
'btn_optimize_cashflow' => 'Optimiser mes Économies',
|
||||
'add_new_provision' => 'Anticiper une grosse dépense à venir',
|
||||
'provision_label' => 'Libellé de la dépense',
|
||||
'provision_placeholder_wood' => 'Ex: Commande de bois, Révision de l\'auto...',
|
||||
'amount' => 'Montant',
|
||||
'expected_date' => 'Date prévue',
|
||||
'btn_add' => 'Ajouter',
|
||||
'upcoming_provisions_list' => 'Dépenses prévues au calendrier',
|
||||
'optimization_assistant_title' => 'Assistant d\'Optimisation des Quinzaines',
|
||||
'error_generic' => 'Une erreur est survenue',
|
||||
'success_add_provision' => 'Dépense prévisionnelle ajoutée !',
|
||||
'no_provisions' => 'Aucune dépense prévisionnelle enregistrée pour le moment.',
|
||||
'btn_delete' => 'Supprimer',
|
||||
'savings_this_month' => 'Économies globales à placer ce mois-ci',
|
||||
'calculate' => 'Calculer la stratégie optimale',
|
||||
'date_past_warning' => 'La date sélectionnée est déjà passée !',
|
||||
'budget_tab_provisions' => 'Provisions',
|
||||
|
||||
// JS & Errors
|
||||
'bud_sav_confirm_delete_month' => 'Estàs segur que vols suprimir totes les dades de %m per a %o?',
|
||||
'bud_sav_prompt_duplicate' => "Vols duplicar les dades de %s cap a %t1?\n\nIntrodueix el nou TOTAL al banc (€) per a %t2:",
|
||||
'bud_err_tech' => 'S\'ha produït un error tècnic.',
|
||||
'bud_err_server' => 'Error del servidor: ',
|
||||
'bud_err_network_dup' => 'Error de xarxa en duplicar.',
|
||||
'bud_sav_saving' => 'Desant...',
|
||||
'bud_err_delete' => 'Error en suprimir.',
|
||||
|
||||
// --- BUDGET : PREVISIONNEL ---
|
||||
'bud_prev_incomes' => 'Ingressos',
|
||||
@@ -564,49 +839,95 @@ return [
|
||||
'bud_prev_err_no_history' => 'Impossible: no hi ha historial disponible per copiar.',
|
||||
'bud_prev_confirm_copy' => "Vols copiar les dades de %s a %t?\n\n⚠️ Això sobreescriurà tots els valors ja presents per a %t.",
|
||||
'bud_prev_confirm_transfers' => "Confirmes que %p ha fet totes les transferències per a %m?\n\nAixò actualitzarà l'Estalvi automàticament.",
|
||||
'bud_prev_target_ph' => 'Ex: 150',
|
||||
'bud_prev_transfer_to' => 'Cap a',
|
||||
|
||||
// --- MODULE BUDGET : AVANCES & TRICOUNT ---
|
||||
'bud_adv_title' => 'Avançaments i Tricount Familiar',
|
||||
'bud_adv_add' => 'Afegir un avançament',
|
||||
'bud_adv_edit' => 'Modificar l\'avançament',
|
||||
'bud_adv_who_paid' => 'Qui ha pagat?',
|
||||
'bud_adv_payer' => 'Pagador',
|
||||
'bud_adv_has_advanced' => '%s ha avançat:',
|
||||
'bud_adv_cc_label' => 'al compte comú',
|
||||
'bud_adv_livret_label' => 'des del seu llibre d\'estalvis',
|
||||
'bud_adv_cc_balance_title' => 'Equilibri del Compte Comú',
|
||||
'bud_adv_owed_to' => 'S\'han de pagar **%s €** a **%s**',
|
||||
'bud_adv_balanced' => 'Els comptes estan perfectament equilibrats!',
|
||||
'bud_adv_saved_badge' => 'Estalvis',
|
||||
'bud_adv_already_saved' => 'Aquests diners provenen d\'un llibre d\'estalvis',
|
||||
'bud_adv_ph_desc' => 'Ex: Compres, Factura d\'electricitat...',
|
||||
'bud_adv_confirm_resolve' => 'Confirmar el reemborsament complet d\'aquest avançament?',
|
||||
'bud_adv_confirm_delete' => 'Eliminar definitivament aquest avançament?',
|
||||
|
||||
// ==========================================
|
||||
// CADEAUX (GIFTS)
|
||||
// MÒDUL: REGALS (gift-list)
|
||||
// ==========================================
|
||||
'gift_page_title' => 'HouseHub - Llista de regals',
|
||||
'gift_occ_tio' => 'Tió',
|
||||
'gift_occ_noel' => 'Nadal',
|
||||
'gift_occ_rois' => 'Reis',
|
||||
'gift_occ_anniv' => 'Aniversari',
|
||||
'gift_occ_sant' => 'Sant',
|
||||
'gift_main_title' => 'Llista de regals %s',
|
||||
'gift_aria_change_view' => 'Canvia la vista',
|
||||
'gift_view_nadal' => 'Nadal',
|
||||
'gift_view_anniv' => 'Aniversaris',
|
||||
'gift_view_by_party' => 'Vista per festa',
|
||||
'gift_no_gifts' => 'No hi ha cap regal registrat per a %s en aquesta vista.',
|
||||
'gift_add_gift' => 'Afegeix un regal',
|
||||
'gift_paid_by' => 'pagat per %s',
|
||||
'gift_summary_title' => 'Resum del pressupost',
|
||||
'gift_col_adult' => 'Adult',
|
||||
'gift_col_child' => 'Infant',
|
||||
'gift_col_party' => 'Festa',
|
||||
'gift_debtor' => 'Deutor',
|
||||
'gift_creditor' => 'Creditor',
|
||||
'gift_liquidations' => 'Tricount',
|
||||
'gift_no_debt' => 'Cap deute pendent.',
|
||||
'gift_owes' => 'ha de pagar',
|
||||
'gift_to' => 'a',
|
||||
'gift_detailed_list' => 'Llista detallada de regals',
|
||||
'gift_col_gift' => 'Regal',
|
||||
'gift_col_link' => 'Enllaç',
|
||||
'gift_modal_title_add' => 'Afegeix un regal per a %s',
|
||||
'gift_modal_title_edit' => 'Edita el regal',
|
||||
'gift_modal_payer' => 'Pagat per',
|
||||
'gift_modal_gift_name' => 'Nom del regal',
|
||||
'gift_modal_ph_name' => 'p. ex., Lego Star Wars',
|
||||
'gift_modal_price' => 'Preu (€)',
|
||||
'gift_modal_link' => 'Enllaç (opcional)',
|
||||
'gift_confirm_delete' => 'Vols eliminar aquest regal?',
|
||||
|
||||
// 1. Títols i capçaleres
|
||||
'gift_page_title' => 'Llista de Regals',
|
||||
'gift_main_title' => 'Regals de %s', // %s serà substituït per l'any (ex: Regals de 2026)
|
||||
'settings' => 'Configuració',
|
||||
|
||||
// 2. Filtres
|
||||
'gift_filter_all_children' => 'Tots els nens',
|
||||
'gift_filter_all_adults' => 'Tots els adults',
|
||||
'gift_empty_state_no_gifts' => 'Cap regal de moment.',
|
||||
'gift_empty_state_no_filter' => 'Cap regal correspon al filtre.',
|
||||
|
||||
// 3. Contingut i estats buits
|
||||
'gift_add_gift' => 'Afegir',
|
||||
'gift_empty_state_no_gifts' => 'Cap regal registrat per a aquesta ocasió.',
|
||||
'gift_empty_state_no_filter' => 'Cap regal coincideix amb els teus filtres.',
|
||||
'gift_paid_by' => 'Pagat per %s', // %s serà substituït pel nom del pagador
|
||||
|
||||
// 4. Secció Tricount (Balanç i Reemborsaments)
|
||||
'gift_liquidations' => 'Balanç i Reemborsaments',
|
||||
'gift_no_debt' => 'Tot està al dia, cap deute!',
|
||||
'gift_owes' => 'deu',
|
||||
'gift_view_matrix' => 'Veure la matriu detallada',
|
||||
'gift_debtor' => 'Deutor',
|
||||
'gift_creditor' => 'Creditor',
|
||||
|
||||
// 5. Modal: Afegir/Modificar Regal
|
||||
'gift_col_adult' => 'Per compte de',
|
||||
'gift_modal_payer' => 'Pagat per',
|
||||
'gift_modal_gift_name' => 'Descripció del regal',
|
||||
'gift_modal_price' => 'Preu estimat / pagat (€)',
|
||||
'gift_modal_link' => 'Enllaç del producte (URL)',
|
||||
'gift_modal_title_add' => 'Afegir per a %s', // JS: %s = nom del nen
|
||||
'gift_modal_title_edit' => 'Modificar el regal', // JS
|
||||
'gift_confirm_delete' => 'Vols eliminar realment aquest regal?', // JS
|
||||
|
||||
// 6. Modal: Configuració de les Festes
|
||||
'gift_settings_title' => 'Configuració de les Festes',
|
||||
'gift_settings_add_title' => '+ Afegir una ocasió',
|
||||
'gift_settings_name_placeholder' => 'Nom (ex: Sant Valentí)',
|
||||
'gift_settings_date_placeholder' => 'MM-DD',
|
||||
'gift_settings_date_help' => 'La data (MM-DD) és opcional. Deixa en blanc per a una festa mòbil.',
|
||||
'gift_settings_empty' => 'Cap festa configurada.', // JS
|
||||
'gift_settings_active' => 'Actiu', // JS
|
||||
|
||||
// 7. Botons genèrics
|
||||
'btn_cancel' => 'Cancel·lar',
|
||||
'btn_save' => 'Desar',
|
||||
'btn_add' => 'Afegir',
|
||||
'loading' => 'Carregant...', //
|
||||
|
||||
// --- MODULE CADEAUX : SETTINGS ---
|
||||
'gift_settings' => 'Configuració',
|
||||
'gift_tab_occasions' => 'Festes i Ocasions',
|
||||
'gift_tab_proches' => 'Familiars (Fora de la llar)',
|
||||
'gift_settings_icon' => 'Icona',
|
||||
'gift_settings_date' => 'Data (MM-DD)',
|
||||
'gift_settings_add_occ' => 'Afegeix una festa',
|
||||
'gift_settings_add_proche'=> 'Afegeix un familiar',
|
||||
'gift_role_proche_adulte'=> 'Adult (Oncle, amic...)',
|
||||
'gift_role_proche_enfant'=> 'Nen (Nebot, neboda...)',
|
||||
'gift_ph_proche_name' => 'Nom',
|
||||
'gift_role_label' => 'Rol',
|
||||
'gift_settings_saved' => 'Configuració desada!',
|
||||
'gift_settings_edit_occ' => 'Modificar la festa',
|
||||
'gift_settings_edit_proche' => 'Modificar el familiar',
|
||||
|
||||
// ==========================================
|
||||
// GARAGE MANAGER
|
||||
// ==========================================
|
||||
|
||||
+357
-38
@@ -157,6 +157,20 @@ return [
|
||||
'mod_planka_desc' => 'Manage your projects in Kanban mode with Planka.',
|
||||
'menu_planka' => 'Planka',
|
||||
|
||||
// --- SETTINGS : KIDS ---
|
||||
'set_kids_title' => 'Household Kids',
|
||||
'set_kids_desc' => 'Manage the list of kids to associate them with savings, gifts, etc.',
|
||||
'set_add_kid_placeholder' => 'Kid\'s first name',
|
||||
'set_btn_add_kid' => 'Add',
|
||||
'set_err_kid_name' => 'The first name is required.',
|
||||
'set_confirm_del_kid' => 'Delete this kid? (Their financial history will remain visible in the archives).',
|
||||
'set_no_kids' => 'No kids configured for this household.',
|
||||
'set_family_config' => 'Family Structure',
|
||||
'set_global_params' => 'Global settings',
|
||||
'set_care_modes' => 'Common childcare arrangements',
|
||||
'set_add_care_mode' => 'Ajouter un modeAdd a mode',
|
||||
'btn_save' => 'Save',
|
||||
|
||||
// ==========================================
|
||||
// FAMILY CALENDAR
|
||||
// ==========================================
|
||||
@@ -200,6 +214,122 @@ return [
|
||||
'fc_err_action' => 'Error during action: ',
|
||||
'fc_today_short' => 'Today',
|
||||
'fc_today_title' => 'Today',
|
||||
'fc_tab_foyer' => '🏡 Household',
|
||||
'fc_tab_members' => '👥 Members & Rights',
|
||||
'fc_select_member' => 'Select a family member:',
|
||||
'fc_role_adult' => 'Adult',
|
||||
'fc_role_child' => 'Child',
|
||||
'fc_care_modes_title' => 'Associated Care Modes',
|
||||
'fc_care_modes_desc' => 'Check the care modes that apply to this child.',
|
||||
'fc_no_care_mode' => 'No care mode configured.',
|
||||
'fc_matrix_title' => '👨👩👦 Leave Rights Matrix',
|
||||
'fc_matrix_desc' => 'Declare your counter codes and renewal dates.',
|
||||
'fc_matrix_code' => 'Code',
|
||||
'fc_matrix_date' => 'Reset Date',
|
||||
'fc_matrix_empty' => 'No counter configured.',
|
||||
'fc_matrix_saved' => 'Rights matrix updated successfully! 💼',
|
||||
'fc_child_saved' => 'Child subscription saved!',
|
||||
'btn_add_counter' => '➕ Add',
|
||||
'btn_save_rights' => '💾 Save',
|
||||
'fc_modal_settings_title' => 'Calendar Settings',
|
||||
'fc_zone_label' => 'School Zone (Holidays)',
|
||||
'fc_zone_desc' => 'Determines the automatic school holiday calendar.',
|
||||
'fc_care_modes_label' => 'Household Care Modes',
|
||||
'fc_care_modes_desc' => 'Manage the global list of possible care modes for the family.',
|
||||
'fc_add_mode_placeholder' => 'E.g., Nanny, Center, Grandparents...',
|
||||
'fc_settings_updated' => 'Settings updated successfully!',
|
||||
'fc_first_day' => 'First day of the week',
|
||||
// --- CALENDAR: SETTINGS (MEMBERS & DISPLAY) ---
|
||||
'fc_tab_display' => '🖥️ Display',
|
||||
'fc_display_title' => 'Display Settings',
|
||||
'fc_display_desc' => 'Customize the default appearance and behavior of your calendar.',
|
||||
'fc_default_view' => 'Default View',
|
||||
'fc_view_month' => '1 month',
|
||||
'fc_view_2months' => '2 months',
|
||||
'fc_view_3months' => '3 months',
|
||||
'fc_working_hours' => 'Default Care Hours',
|
||||
'fc_working_hours_desc' => 'Example: 08:30-18:00',
|
||||
|
||||
'fc_role_unknown' => 'Unknown',
|
||||
'fc_role_adult' => 'Adult',
|
||||
'fc_role_child' => 'Child',
|
||||
'fc_role_helper' => 'Helper',
|
||||
'fc_error' => 'Error: ',
|
||||
|
||||
'fc_care_modes_title' => 'Default Care Modes',
|
||||
'fc_care_modes_desc' => 'Which care modes apply to this child?',
|
||||
'fc_no_care_mode' => 'No care mode configured for this household.',
|
||||
'fc_child_saved' => 'Saved!',
|
||||
|
||||
'fc_matrix_title' => 'Leave Matrix',
|
||||
'fc_matrix_desc' => 'Define quotas and renewal dates.',
|
||||
'fc_matrix_empty' => 'No counters defined.',
|
||||
"fc_matrix_saved" => "Matrix saved!",
|
||||
|
||||
'fc_leave_obsolete' => 'Obsolete',
|
||||
'fc_leave_code_placeholder' => 'Code...',
|
||||
'fc_leave_method_fixed' => 'Fixed',
|
||||
'fc_leave_method_accumulated' => 'Accrued',
|
||||
|
||||
'fc_date_format_placeholder' => 'DD/MM',
|
||||
'fc_date_format_title' => 'DD/MM Format',
|
||||
|
||||
'fc_table_code' => 'Code',
|
||||
'fc_table_method' => 'Method',
|
||||
'fc_table_quota' => 'Quota',
|
||||
'fc_table_renewal' => 'Renewal',
|
||||
|
||||
'btn_save_rights' => 'Save',
|
||||
|
||||
// ==========================================
|
||||
// CALENDAR SETTINGS MODAL
|
||||
// ==========================================
|
||||
'fc_modal_settings_title' => 'Calendar Settings',
|
||||
'fc_tab_foyer' => 'Household',
|
||||
'fc_tab_members' => 'Members & Rights',
|
||||
|
||||
// Household: School Zone
|
||||
'fc_zone_label' => 'School zone',
|
||||
'fc_zone_a' => 'Zone A',
|
||||
'fc_zone_b' => 'Zone B',
|
||||
'fc_zone_c' => 'Zone C',
|
||||
'fc_zone_other' => 'Other / Outside France',
|
||||
'fc_zone_desc' => 'Used to accurately display school holidays.',
|
||||
|
||||
// Household: Care Modes
|
||||
'fc_care_modes_label' => 'Childcare options',
|
||||
'fc_add_mode_placeholder' => 'Ex: Nanny, Daycare...',
|
||||
'fc_care_modes_desc' => 'Add the different childcare options for your children.',
|
||||
'fc_empty_kids_msg' => 'No children are configured in your household yet.',
|
||||
'fc_link_settings' => 'Go to general settings',
|
||||
|
||||
// Household: Leave Catalog
|
||||
'fc_leave_catalog_title' => 'Leave Catalog',
|
||||
'fc_leave_catalog_desc' => 'Define the possible leave types for the whole family.',
|
||||
'fc_add_leave_type' => 'Add a leave type',
|
||||
'fc_col_code' => 'Code',
|
||||
'fc_code_irreversible' => 'Irreversible',
|
||||
'fc_col_label' => 'Label',
|
||||
'fc_leave_label_ph' => 'Ex: PTO, Sick leave...',
|
||||
'fc_quota_default' => 'Default quota',
|
||||
'fc_reset_month' => 'Reset month',
|
||||
|
||||
// Members
|
||||
'fc_select_member' => 'Select a member:',
|
||||
|
||||
// Months
|
||||
'month_jan' => 'January',
|
||||
'month_feb' => 'February',
|
||||
'month_mar' => 'March',
|
||||
'month_apr' => 'April',
|
||||
'month_may' => 'May',
|
||||
'month_jun' => 'June',
|
||||
'month_jul' => 'July',
|
||||
'month_aug' => 'August',
|
||||
'month_sep' => 'September',
|
||||
'month_oct' => 'October',
|
||||
'month_nov' => 'November',
|
||||
'month_dec' => 'December',
|
||||
|
||||
'leg_presence' => 'Pep presence',
|
||||
'leg_school_holidays' => 'School holidays',
|
||||
@@ -209,6 +339,9 @@ return [
|
||||
'leg_centre' => 'Centre',
|
||||
'leg_avis' => 'Notice',
|
||||
'leg_pep_sick' => 'Pep sick',
|
||||
'care_nounou' => 'Nanny',
|
||||
'care_centre' => 'Daycare',
|
||||
'care_avis' => 'Grandparents',
|
||||
|
||||
'vac_toussaint' => 'All Saints\' holidays',
|
||||
'vac_noel' => 'Christmas holidays',
|
||||
@@ -383,6 +516,86 @@ return [
|
||||
'cat_fixed' => 'Fixed costs',
|
||||
'cat_others' => 'Other / Unexpected',
|
||||
'cat_savings' => 'Savings',
|
||||
'budget_tab_kids' => 'Kids',
|
||||
'budget_settings_title' => 'Budget Settings',
|
||||
'bs_tab_accounts' => '💳 Accounts',
|
||||
'bs_tab_categories' => '🏷️ Categories',
|
||||
'bs_tab_rules' => '🤖 Import Rules',
|
||||
'bs_tab_salaries' => '💼 Salaries',
|
||||
'bs_empty_accounts' => 'No accounts configured.',
|
||||
'bs_type_checking' => 'Checking',
|
||||
'bs_type_savings' => 'Savings',
|
||||
'bs_badge_default' => 'Default',
|
||||
'bs_owner' => 'Owner',
|
||||
'bs_shared_account' => 'Joint Account',
|
||||
'bs_empty_categories' => 'No categories configured.',
|
||||
'bs_type_income' => 'Income',
|
||||
'bs_type_expense' => 'Expense',
|
||||
'bs_type_savings_cat' => 'Savings',
|
||||
'bs_empty_rules' => 'No import rules configured.',
|
||||
'bs_empty_salaries' => 'No salary defined for the year',
|
||||
'bs_salaries_year_desc' => 'Configurations for the year',
|
||||
'bs_salary_net' => 'Net salary',
|
||||
'bs_salary_fees' => 'Monthly payment / Fees',
|
||||
'bs_add_account_title' => 'Add an account',
|
||||
'bs_account_name_ph' => 'Name (e.g., Baby Savings)',
|
||||
'bs_confirm_delete' => 'Are you sure you want to delete this account?',
|
||||
'bs_add_category_title' => '+ Add a category',
|
||||
'bs_cat_code_ph' => 'Code (e.g., FOOD)',
|
||||
'bs_cat_label_ph' => 'Label (e.g., Groceries)',
|
||||
'bs_cat_icon_ph' => 'Icon (e.g., 🛒)',
|
||||
'bs_confirm_delete_cat' => 'Are you sure you want to delete this category?',
|
||||
'bs_add_rule_title' => 'Add a rule',
|
||||
'bs_rule_keyword_ph' => 'Keyword (e.g., WALMART)',
|
||||
'bs_confirm_delete_rule'=> 'Are you sure you want to delete this rule?',
|
||||
'bs_currency_label' => 'Household Currency',
|
||||
'bs_currency_desc' => 'Main currency used for displaying your accounts and budgets.',
|
||||
'bs_currency_updated' => 'Currency updated successfully!',
|
||||
// ONGLET CSV MAPPING
|
||||
'bs_csv_title' => 'CSV Import Format',
|
||||
'bs_csv_desc' => 'Configure the columns that match your bank\'s export file.',
|
||||
'bs_csv_delimiter' => 'Delimiter',
|
||||
'bs_csv_delim_semi' => 'Semicolon (;)',
|
||||
'bs_csv_delim_comma' => 'Comma (,)',
|
||||
'bs_csv_delim_tab' => 'Tab',
|
||||
'bs_csv_date_format' => 'Date Format',
|
||||
'bs_csv_col_index' => 'Column Indexes',
|
||||
'bs_csv_col_index_help' => '(Note: the 1st column is Number 0)',
|
||||
'bs_csv_col_date' => 'Col #: Date',
|
||||
'bs_csv_col_label' => 'Col #: Label / Description',
|
||||
'bs_csv_amount_mgmt' => 'Amount Management',
|
||||
'bs_csv_amount_single' => 'Single column (Positive = Income, Negative = Expense)',
|
||||
'bs_csv_amount_split' => 'Two separate columns (Debit / Credit)',
|
||||
'bs_csv_col_amount' => 'Col #: Amount',
|
||||
'bs_csv_col_debit' => 'Col #: Debit',
|
||||
'bs_csv_col_credit' => 'Col #: Credit',
|
||||
'bs_csv_col_ref' => 'Col #: Unique Reference (Optional)',
|
||||
'btn_save_format' => 'Save format',
|
||||
'bs_csv_saved' => 'CSV format saved',
|
||||
'bs_tab_csv' => '📥 CSV Import',
|
||||
'bs_csv_settings_title' => 'Format Configuration',
|
||||
'bs_csv_drop_title' => 'Drop your CSV file here',
|
||||
'bs_csv_drop_desc' => 'or click to browse your files',
|
||||
'bs_csv_err_type' => 'Error: Only .csv files are allowed.',
|
||||
'bs_csv_file_ok' => 'File loaded',
|
||||
'bs_csv_preview_title' => 'Columns Preview',
|
||||
'bs_csv_empty_col' => 'Empty',
|
||||
'bs_csv_no_data' => 'No transaction row found in this file.',
|
||||
|
||||
// ONBOARDING BUDGET
|
||||
'bs_onb_title' => 'Welcome! Let\'s set up your budget:',
|
||||
'bs_onb_desc' => 'Complete these steps so the app can calculate your remaining budget:',
|
||||
'bs_onb_cat_link' => 'Categories',
|
||||
'bs_onb_cat_desc' => 'Create at least one expense category.',
|
||||
'bs_onb_acc_link' => 'Bank Accounts',
|
||||
'bs_onb_acc_desc' => 'Add at least one checking or savings account.',
|
||||
'bs_onb_sal_link' => 'Salaries',
|
||||
'bs_onb_sal_desc' => 'Enter your income for the current year.',
|
||||
'bs_onb_items_link' => 'Budget Items',
|
||||
'bs_onb_items_desc' => 'Go to the Summary tab to list your fixed expenses (Rent, Insurance...).',
|
||||
'bs_rule_search_ph' => '🔍 Search for a keyword (e.g. WALMART)...',
|
||||
'error_loading_settings' => 'Error loading settings',
|
||||
'settings' => 'Settings',
|
||||
|
||||
// --- BUDGET: TRACKING ---
|
||||
'bud_rem_school' => 'School (estimated remaining)',
|
||||
@@ -482,6 +695,9 @@ return [
|
||||
'bud_reg_none' => 'None',
|
||||
'bud_recap_confirm_delete' => 'Are you sure you want to delete this item?',
|
||||
'bud_err_delete' => 'Error while deleting.',
|
||||
'bud_prev_monthly_target' => 'Target amount / Monthly payment (€)',
|
||||
'bud_prev_transfer_dest' => 'Transfer destination',
|
||||
'bud_prev_none' => 'No destination (Standard expense)',
|
||||
|
||||
// --- BUDGET: SAVINGS ---
|
||||
'bud_sav_add_month' => 'Enter a month',
|
||||
@@ -512,6 +728,39 @@ return [
|
||||
'bud_sav_prompt_duplicate' => "Do you want to copy data from %s to %t1?\n\nEnter the new TOTAL bank balance (€) for %t2:",
|
||||
'bud_err_server' => 'Server error: ',
|
||||
'bud_err_network_dup' => 'Network error during duplication.',
|
||||
'budget_tab_kids' => 'Kids 👶',
|
||||
'bud_sav_add_one_month' => '+1 Month',
|
||||
'bud_sav_add_month' => 'Add a month',
|
||||
'bud_sav_no_data' => 'No savings data for %s.',
|
||||
'bud_sav_post_month' => 'Item / Month',
|
||||
'bud_sav_from_date' => 'From %s',
|
||||
'bud_sav_edit_modal' => 'Edit this month',
|
||||
'bud_sav_delete_month' => 'Delete this month',
|
||||
'bud_sav_total_bank' => 'TOTAL BANK',
|
||||
'bud_sav_extra' => 'UNALLOCATED (EXTRAS)',
|
||||
'bud_sav_modal_title_add' => 'Add a month',
|
||||
'bud_sav_modal_title_edit' => 'Edit',
|
||||
'bud_sav_month_concerned' => 'Target month',
|
||||
'bud_sav_total_bank_eur' => 'Total in Bank (€)',
|
||||
'bud_sav_ventilation' => 'Accounts breakdown',
|
||||
'bud_sav_adj_help' => 'Enter adjustments (+/-) to auto-calculate.',
|
||||
'bud_sav_add_line' => 'Add line',
|
||||
'bud_category' => 'Category',
|
||||
'bud_sav_current' => 'Current',
|
||||
'bud_sav_adjust' => 'Adjustment',
|
||||
'bud_sav_new' => 'New',
|
||||
'bud_sav_sum_mode_title' => 'Toggle calculator',
|
||||
'bud_sav_selection' => 'Selection:',
|
||||
'bud_sav_ph_name' => 'Account name',
|
||||
|
||||
// JS & Errors
|
||||
'bud_sav_confirm_delete_month' => 'Are you sure you want to delete all data of %m for %o?',
|
||||
'bud_sav_prompt_duplicate' => "Do you want to duplicate data from %s to %t1?\n\nEnter the new BANK TOTAL (€) for %t2:",
|
||||
'bud_err_tech' => 'A technical error occurred.',
|
||||
'bud_err_server' => 'Server error: ',
|
||||
'bud_err_network_dup' => 'Network error during duplication.',
|
||||
'bud_sav_saving' => 'Saving...',
|
||||
'bud_err_delete' => 'Error during deletion.',
|
||||
|
||||
// --- BUDGET: FORECAST ---
|
||||
'bud_prev_incomes' => 'Income',
|
||||
@@ -548,50 +797,120 @@ return [
|
||||
'bud_prev_confirm_copy' => "Do you want to copy data from %s to %t?\n\n⚠️ This will overwrite all values already present for %t.",
|
||||
'bud_prev_confirm_transfers' => "Confirm that %p has made all their transfers for %m?\n\nThis will automatically update Savings.",
|
||||
'bud_prev_label_name' => 'Name',
|
||||
'bud_adv_title' => 'Advances & Family Tricount',
|
||||
'bud_adv_add' => 'Add an Advance',
|
||||
'bud_adv_edit' => 'Edit Advance',
|
||||
'bud_adv_who_paid' => 'Who paid?',
|
||||
'bud_adv_payer' => 'Payer',
|
||||
'bud_adv_has_advanced' => '%s advanced:',
|
||||
'bud_adv_cc_label' => 'on the joint account',
|
||||
'bud_adv_livret_label' => 'from their savings account',
|
||||
'bud_adv_cc_balance_title' => 'Joint Account Balance',
|
||||
'bud_adv_owed_to' => '<strong>%s €</strong> are owed to <strong>%s</strong>',
|
||||
'bud_adv_balanced' => 'Accounts are perfectly balanced!',
|
||||
'bud_adv_saved_badge' => 'Savings',
|
||||
'bud_adv_already_saved' => 'This money comes from a savings account',
|
||||
'bud_adv_ph_desc' => 'e.g., Groceries, Electricity bill...',
|
||||
'bud_adv_confirm_resolve' => 'Confirm the complete reimbursement of this advance?',
|
||||
'bud_adv_confirm_delete' => 'Permanently delete this advance?',
|
||||
'date' => 'Date',
|
||||
'bud_label_name' => 'Description',
|
||||
'bud_amount' => 'Amount',
|
||||
'actions' => 'Actions',
|
||||
'edit' => 'Edit',
|
||||
'delete' => 'Delete',
|
||||
'btn_cancel' => 'Cancel',
|
||||
'btn_save' => 'Save',
|
||||
'bud_prev_target_ph' => 'E.g., 150',
|
||||
'bud_prev_transfer_to' => 'To',
|
||||
|
||||
// --- Module Budget : Provisions & Optimisation ---
|
||||
'budget_provisions_title' => 'Provisions & Cashflow Optimization',
|
||||
'btn_optimize_cashflow' => 'Optimize My Savings',
|
||||
'add_new_provision' => 'Anticipate an upcoming major expense',
|
||||
'provision_label' => 'Expense description',
|
||||
'provision_placeholder_wood' => 'E.g., Firewood order, Car service...',
|
||||
'amount' => 'Amount',
|
||||
'expected_date' => 'Expected date',
|
||||
'btn_add' => 'Add',
|
||||
'upcoming_provisions_list' => 'Scheduled expenses',
|
||||
'optimization_assistant_title' => 'Fortnightly Interest Optimization Assistant',
|
||||
'error_generic' => 'An error occurred',
|
||||
'success_add_provision' => 'Forecasted expense added successfully!',
|
||||
'no_provisions' => 'No forecasted expenses registered at the moment.',
|
||||
'btn_delete' => 'Delete',
|
||||
'savings_this_month' => 'Total savings to allocate this month',
|
||||
'calculate' => 'Calculate optimal strategy',
|
||||
'date_past_warning' => 'The selected date has already passed!',
|
||||
'budget_tab_provisions' => 'Forecasts',
|
||||
|
||||
// ==========================================
|
||||
// GIFTS
|
||||
// MODULE: GIFTS (gift-list)
|
||||
// ==========================================
|
||||
'gift_page_title' => 'HouseHub - Gift list',
|
||||
'gift_occ_tio' => 'Tió',
|
||||
'gift_occ_noel' => 'Christmas',
|
||||
'gift_occ_rois' => 'Three Kings',
|
||||
'gift_occ_anniv' => 'Birthday',
|
||||
'gift_occ_sant' => 'Name day',
|
||||
'gift_main_title' => 'Gift list %s',
|
||||
'gift_aria_change_view' => 'Change view',
|
||||
'gift_view_nadal' => 'Christmas',
|
||||
'gift_view_anniv' => 'Birthdays',
|
||||
'gift_view_by_party' => 'View by occasion',
|
||||
'gift_no_gifts' => 'No gifts recorded for %s in this view.',
|
||||
'gift_add_gift' => 'Add a gift',
|
||||
'gift_paid_by' => 'paid by %s',
|
||||
'gift_summary_title' => 'Budget summary',
|
||||
'gift_col_adult' => 'Adult',
|
||||
'gift_col_child' => 'Child',
|
||||
'gift_col_party' => 'Occasion',
|
||||
'gift_debtor' => 'Debtor',
|
||||
'gift_creditor' => 'Creditor',
|
||||
'gift_liquidations' => 'Settlement',
|
||||
'gift_no_debt' => 'No outstanding debts.',
|
||||
'gift_owes' => 'owes',
|
||||
'gift_to' => 'to',
|
||||
'gift_detailed_list' => 'Detailed gift list',
|
||||
'gift_col_gift' => 'Gift',
|
||||
'gift_col_link' => 'Link',
|
||||
'gift_modal_title_add' => 'Add a gift for %s',
|
||||
'gift_modal_title_edit' => 'Edit gift',
|
||||
'gift_modal_payer' => 'Paid by',
|
||||
'gift_modal_gift_name' => 'Gift name',
|
||||
'gift_modal_ph_name' => 'e.g. Lego Star Wars',
|
||||
'gift_modal_price' => 'Price (€)',
|
||||
'gift_modal_link' => 'Link (optional)',
|
||||
'gift_confirm_delete' => 'Are you sure you want to delete this gift?',
|
||||
|
||||
// 1. Titles and headers
|
||||
'gift_page_title' => 'Gift List',
|
||||
'gift_main_title' => 'Gifts of %s', // %s will be replaced by the year (e.g., Gifts of 2026)
|
||||
'settings' => 'Settings',
|
||||
|
||||
// 2. Filters
|
||||
'gift_filter_all_children' => 'All children',
|
||||
'gift_filter_all_adults' => 'All adults',
|
||||
'gift_empty_state_no_gifts' => 'No gifts yet.',
|
||||
'gift_empty_state_no_filter' => 'No gifts match the filter.',
|
||||
|
||||
// 3. Content and empty states
|
||||
'gift_add_gift' => 'Add',
|
||||
'gift_empty_state_no_gifts' => 'No gifts recorded for this occasion.',
|
||||
'gift_empty_state_no_filter' => 'No gifts match your filters.',
|
||||
'gift_paid_by' => 'Paid by %s', // %s will be replaced by the payer's name
|
||||
|
||||
// 4. Tricount Section (Balance & Refunds)
|
||||
'gift_liquidations' => 'Balance & Refunds',
|
||||
'gift_no_debt' => 'Everything is settled, no debts!',
|
||||
'gift_owes' => 'owes',
|
||||
'gift_view_matrix' => 'View detailed matrix',
|
||||
'gift_debtor' => 'Debtor',
|
||||
'gift_creditor' => 'Creditor',
|
||||
|
||||
// 5. Modal: Add/Edit Gift
|
||||
'gift_col_adult' => 'On behalf of',
|
||||
'gift_modal_payer' => 'Paid by',
|
||||
'gift_modal_gift_name' => 'Gift description',
|
||||
'gift_modal_price' => 'Estimated / paid price (€)',
|
||||
'gift_modal_link' => 'Product link (URL)',
|
||||
'gift_modal_title_add' => 'Add for %s', // JS: %s = child's name
|
||||
'gift_modal_title_edit' => 'Edit gift', // JS
|
||||
'gift_confirm_delete' => 'Are you sure you want to delete this gift?', // JS
|
||||
|
||||
// 6. Modal: Occasions Settings
|
||||
'gift_settings_title' => 'Occasions Settings',
|
||||
'gift_settings_add_title' => '+ Add an occasion',
|
||||
'gift_settings_name_placeholder' => 'Name (e.g. Valentine\'s Day)',
|
||||
'gift_settings_date_placeholder' => 'MM-DD',
|
||||
'gift_settings_date_help' => 'Date (MM-DD) is optional. Leave empty for a floating date.',
|
||||
'gift_settings_empty' => 'No occasion configured.', // JS
|
||||
'gift_settings_active' => 'Active', // JS
|
||||
|
||||
// 7. Generic buttons
|
||||
'btn_cancel' => 'Cancel',
|
||||
'btn_save' => 'Save',
|
||||
'btn_add' => 'Add',
|
||||
'loading' => 'Loading...',
|
||||
|
||||
// --- MODULE CADEAUX : SETTINGS ---
|
||||
'gift_settings' => 'Settings',
|
||||
'gift_tab_occasions' => 'Occasions & Events',
|
||||
'gift_tab_proches' => 'Relatives (Outside household)',
|
||||
'gift_settings_icon' => 'Icon',
|
||||
'gift_settings_date' => 'Date (MM-DD)',
|
||||
'gift_settings_add_occ' => 'Add an occasion',
|
||||
'gift_settings_add_proche'=> 'Add a relative',
|
||||
'gift_role_proche_adulte'=> 'Adult (Uncle, friend...)',
|
||||
'gift_role_proche_enfant'=> 'Child (Nephew, niece...)',
|
||||
'gift_ph_proche_name' => 'First name',
|
||||
'gift_role_label' => 'Role',
|
||||
'gift_settings_saved' => 'Settings saved!',
|
||||
'gift_settings_edit_occ' => 'Edit occasion',
|
||||
'gift_settings_edit_proche' => 'Edit relative',
|
||||
|
||||
// ==========================================
|
||||
// GARAGE MANAGER
|
||||
|
||||
+365
-38
@@ -156,6 +156,20 @@ return [
|
||||
'mod_planka_desc' => 'Gérez vos projets en mode Kanban avec Planka.',
|
||||
'menu_planka' => 'Planka',
|
||||
|
||||
// --- SETTINGS : ENFANTS ---
|
||||
'set_kids_title' => 'Enfants du foyer',
|
||||
'set_kids_desc' => 'Gérez la liste des enfants pour les associer à l\'épargne, aux cadeaux, etc.',
|
||||
'set_add_kid_placeholder' => 'Prénom de l\'enfant',
|
||||
'set_btn_add_kid' => 'Ajouter',
|
||||
'set_err_kid_name' => 'Le prénom est requis.',
|
||||
'set_confirm_del_kid' => 'Supprimer cet enfant ? (Son historique financier restera visible dans les archives).',
|
||||
'set_no_kids' => 'Aucun enfant configuré pour ce foyer.',
|
||||
'set_family_config' => 'Configuration du foyer',
|
||||
'set_global_params' => 'Paramètres globaux',
|
||||
'set_care_modes' => 'Modes de garde usuels',
|
||||
'set_add_care_mode' => 'Ajouter un mode',
|
||||
'btn_save_config' => 'Enregistrer',
|
||||
|
||||
// ==========================================
|
||||
// FAMILY CALENDAR
|
||||
// ==========================================
|
||||
@@ -199,6 +213,126 @@ return [
|
||||
'fc_err_action' => 'Erreur lors de l\'action : ',
|
||||
'fc_today_short' => 'Auj.',
|
||||
'fc_today_title' => "Aujourd'hui",
|
||||
'care_nounou' => 'Nounou',
|
||||
'care_centre' => 'Centre de loisirs',
|
||||
'care_avis' => 'Avis',
|
||||
'fc_tab_foyer' => '🏡 Foyer',
|
||||
'fc_tab_members' => '👥 Membres & Droits',
|
||||
'fc_select_member' => 'Sélectionner un membre de la famille :',
|
||||
'fc_role_adult' => 'Adulte',
|
||||
'fc_role_child' => 'Enfant',
|
||||
'fc_care_modes_title' => 'Modes de garde associés',
|
||||
'fc_care_modes_desc' => 'Cochez les modes de garde qui concernent cet enfant.',
|
||||
'fc_no_care_mode' => 'Aucun mode configuré.',
|
||||
'fc_matrix_title' => '👨👩👦 Matrice des Droits aux Congés',
|
||||
'fc_matrix_desc' => 'Déclarez les codes de compteurs et dates de renouvellement.',
|
||||
'fc_matrix_code' => 'Code',
|
||||
'fc_matrix_date' => 'Date Reset',
|
||||
'fc_matrix_empty' => 'Aucun compteur configuré.',
|
||||
'fc_matrix_saved' => 'Matrice des droits mise à jour ! 💼',
|
||||
'fc_child_saved' => 'Abonnement enfant enregistré !',
|
||||
'btn_add_counter' => '➕ Ajouter',
|
||||
'btn_save_rights' => '💾 Enregistrer',
|
||||
'fc_modal_settings_title' => 'Paramètres du Calendrier',
|
||||
'fc_zone_label' => 'Zone Scolaire (Vacances)',
|
||||
'fc_zone_desc' => 'Détermine le calendrier automatique des vacances scolaires.',
|
||||
'fc_care_modes_label' => 'Modes de Garde du Foyer',
|
||||
'fc_care_modes_desc' => 'Gérez la liste globale des modes de garde possibles pour la famille.',
|
||||
'fc_add_mode_placeholder' => 'Ex: Nounou, Casal, Grands-parents...',
|
||||
'fc_settings_updated' => 'Paramètres mis à jour avec succès !',
|
||||
'fc_first_day' => 'Premier jour de la semaine',
|
||||
// --- CALENDRIER : PARAMÈTRES (MEMBRES & AFFICHAGE) ---
|
||||
'fc_tab_display' => '🖥️ Affichage',
|
||||
'fc_display_title' => 'Paramètres d\'affichage',
|
||||
'fc_display_desc' => 'Personnalisez l\'apparence et le comportement par défaut de votre calendrier.',
|
||||
'fc_default_view' => 'Vue par défaut',
|
||||
'fc_view_month' => '1 mois',
|
||||
'fc_view_2months' => '2 mois',
|
||||
'fc_view_3months' => '3 mois',
|
||||
'fc_working_hours' => 'Horaires de garde (Défaut)',
|
||||
'fc_working_hours_desc' => 'Exemple : 08:30-18:00',
|
||||
|
||||
'fc_role_unknown' => 'Inconnu',
|
||||
'fc_role_adult' => 'Adulte',
|
||||
'fc_role_child' => 'Enfant',
|
||||
'fc_role_helper' => 'Intervenant',
|
||||
'fc_error' => 'Erreur : ',
|
||||
|
||||
'fc_care_modes_title' => 'Modes de Garde Par Défaut',
|
||||
'fc_care_modes_desc' => 'Quels modes de garde s\'appliquent à cet enfant ?',
|
||||
'fc_no_care_mode' => 'Aucun mode de garde configuré dans le foyer.',
|
||||
'fc_child_saved' => 'Sauvegardé !',
|
||||
|
||||
'fc_matrix_title' => 'Matrice des Congés',
|
||||
'fc_matrix_desc' => 'Définissez les quotas et la date de renouvellement.',
|
||||
'fc_matrix_empty' => 'Aucun compteur défini.',
|
||||
'fc_matrix_saved' => 'Matrice enregistrée !',
|
||||
|
||||
'fc_leave_obsolete' => 'Obsolète',
|
||||
'fc_leave_code_placeholder' => 'Code...',
|
||||
'fc_leave_method_fixed' => 'Fixe',
|
||||
'fc_leave_method_accumulated' => 'Cumul',
|
||||
|
||||
'fc_date_format_placeholder' => 'JJ/MM',
|
||||
'fc_date_format_title' => 'Format JJ/MM',
|
||||
|
||||
'fc_table_code' => 'Code',
|
||||
'fc_table_method' => 'Méthode',
|
||||
'fc_table_quota' => 'Quota',
|
||||
'fc_table_renewal' => 'Renouv.',
|
||||
|
||||
'btn_save_rights' => 'Enregistrer',
|
||||
|
||||
// ==========================================
|
||||
// MODALE PARAMÈTRES CALENDRIER
|
||||
// ==========================================
|
||||
'fc_modal_settings_title' => 'Paramètres du calendrier',
|
||||
'fc_tab_foyer' => 'Foyer',
|
||||
'fc_tab_members' => 'Membres & Droits',
|
||||
|
||||
// Foyer : Zone scolaire
|
||||
'fc_zone_label' => 'Zone scolaire',
|
||||
'fc_zone_a' => 'Zone A',
|
||||
'fc_zone_b' => 'Zone B',
|
||||
'fc_zone_c' => 'Zone C',
|
||||
'fc_zone_other' => 'Autre / Hors France',
|
||||
'fc_zone_desc' => 'Permet d\'afficher correctement les vacances scolaires.',
|
||||
|
||||
// Foyer : Modes de garde
|
||||
'fc_care_modes_label' => 'Modes de garde',
|
||||
'fc_add_mode_placeholder' => 'Ex: Nounou, Crèche...',
|
||||
'fc_care_modes_desc' => 'Ajoutez les différents modes de garde pour vos enfants.',
|
||||
'fc_empty_kids_msg' => 'Aucun enfant n\'est configuré dans votre famille pour le moment.',
|
||||
'fc_link_settings' => 'Aller aux paramètres généraux',
|
||||
|
||||
// Foyer : Catalogue des congés
|
||||
'fc_leave_catalog_title' => 'Catalogue des congés',
|
||||
'fc_leave_catalog_desc' => 'Définissez les types de congés possibles pour toute la famille.',
|
||||
'fc_add_leave_type' => 'Ajouter un type de congé',
|
||||
'fc_col_code' => 'Code',
|
||||
'fc_code_irreversible' => 'Irréversible',
|
||||
'fc_col_label' => 'Label',
|
||||
'fc_leave_label_ph' => 'Ex: RTT, Récup...',
|
||||
'fc_quota_default' => 'Quota par défaut',
|
||||
'fc_reset_month' => 'Mois de renouv.',
|
||||
|
||||
// Membres
|
||||
'fc_select_member' => 'Sélectionner un membre :',
|
||||
|
||||
// Mois (S'ils n'existent pas déjà dans ton système)
|
||||
'month_jan' => 'Janvier',
|
||||
'month_feb' => 'Février',
|
||||
'month_mar' => 'Mars',
|
||||
'month_apr' => 'Avril',
|
||||
'month_may' => 'Mai',
|
||||
'month_jun' => 'Juin',
|
||||
'month_jul' => 'Juillet',
|
||||
'month_aug' => 'Août',
|
||||
'month_sep' => 'Septembre',
|
||||
'month_oct' => 'Octobre',
|
||||
'month_nov' => 'Novembre',
|
||||
'month_dec' => 'Décembre',
|
||||
|
||||
|
||||
'leg_presence' => 'Présence Pep',
|
||||
'leg_school_holidays' => 'Vacances Scolaires',
|
||||
@@ -385,6 +519,71 @@ return [
|
||||
'cat_fixed' => 'Charges Fixes',
|
||||
'cat_others' => 'Autres / Imprévus',
|
||||
'cat_savings' => 'Épargne',
|
||||
'budget_tab_kids' => 'Enfants',
|
||||
'budget_settings_title' => 'Paramètres du Budget',
|
||||
'bs_tab_accounts' => '💳 Comptes',
|
||||
'bs_tab_categories' => '🏷️ Catégories',
|
||||
'bs_tab_rules' => '🤖 Règles d\'import',
|
||||
'bs_tab_salaries' => '💼 Salaires',
|
||||
'bs_empty_accounts' => 'Aucun compte configuré.',
|
||||
'bs_type_checking' => 'Courant',
|
||||
'bs_type_savings' => 'Épargne',
|
||||
'bs_badge_default' => 'Défaut',
|
||||
'bs_owner' => 'Propriétaire',
|
||||
'bs_shared_account' => 'Compte Commun',
|
||||
'bs_empty_categories' => 'Aucune catégorie configurée.',
|
||||
'bs_type_income' => 'Revenu',
|
||||
'bs_type_expense' => 'Dépense',
|
||||
'bs_type_savings_cat' => 'Épargne',
|
||||
'bs_empty_rules' => 'Aucune règle d\'importation.',
|
||||
'bs_empty_salaries' => 'Aucun salaire défini pour l\'année',
|
||||
'bs_salaries_year_desc' => 'Configurations pour l\'année',
|
||||
'bs_salary_net' => 'Salaire net',
|
||||
'bs_salary_fees' => 'Mensualité / Frais',
|
||||
'bs_add_account_title' => '+ Ajouter un compte',
|
||||
'bs_account_name_ph' => 'Nom (ex: Livret A Bébé)',
|
||||
'bs_confirm_delete' => 'Voulez-vous vraiment supprimer ce compte ?',
|
||||
'bs_add_category_title' => '+ Ajouter une catégorie',
|
||||
'bs_cat_code_ph' => 'Code (ex: FOOD)',
|
||||
'bs_cat_label_ph' => 'Libellé (ex: Courses)',
|
||||
'bs_cat_icon_ph' => 'Icône (ex: 🛒)',
|
||||
'bs_confirm_delete_cat' => 'Voulez-vous vraiment supprimer cette catégorie ?',
|
||||
'bs_add_rule_title' => 'Ajouter une règle',
|
||||
'bs_rule_keyword_ph' => 'Mot-clé (ex: AUCHAN)',
|
||||
'bs_confirm_delete_rule' => 'Voulez-vous vraiment supprimer cette règle ?',
|
||||
'bs_currency_label' => 'Devise du Foyer',
|
||||
'bs_currency_desc' => 'Devise principale utilisée pour l\'affichage de vos comptes et budgets.',
|
||||
'bs_currency_updated' => 'Devise mise à jour avec succès !',
|
||||
// ONGLET CSV MAPPING
|
||||
'bs_csv_title' => 'Format de l\'import CSV',
|
||||
'bs_csv_desc' => 'Configurez ici les colonnes correspondantes à l\'export de votre banque.',
|
||||
'bs_csv_delimiter' => 'Séparateur',
|
||||
'bs_csv_delim_semi' => 'Point-virgule (;)',
|
||||
'bs_csv_delim_comma' => 'Virgule (,)',
|
||||
'bs_csv_delim_tab' => 'Tabulation',
|
||||
'bs_csv_date_format' => 'Format de la Date',
|
||||
'bs_csv_col_index' => 'Index des colonnes',
|
||||
'bs_csv_col_index_help' => '(Attention : la 1ère colonne est la Numéro 0)',
|
||||
'bs_csv_col_date' => 'N° Col : Date',
|
||||
'bs_csv_col_label' => 'N° Col : Libellé / Motif',
|
||||
'bs_csv_amount_mgmt' => 'Gestion des montants',
|
||||
'bs_csv_amount_single' => 'Une seule colonne (Positif = Revenu, Négatif = Dépense)',
|
||||
'bs_csv_amount_split' => 'Deux colonnes séparées (Débit / Crédit)',
|
||||
'bs_csv_col_amount' => 'N° Col : Montant',
|
||||
'bs_csv_col_debit' => 'N° Col : Débit',
|
||||
'bs_csv_col_credit' => 'N° Col : Crédit',
|
||||
'bs_csv_col_ref' => 'N° Col : Référence unique (Optionnel)',
|
||||
'btn_save_format' => 'Enregistrer le format',
|
||||
'bs_csv_saved' => 'Format CSV enregistré',
|
||||
'bs_tab_csv' => '📥 Import CSV',
|
||||
'bs_csv_settings_title' => 'Configuration du format',
|
||||
'bs_csv_drop_title' => 'Glissez votre fichier CSV ici',
|
||||
'bs_csv_drop_desc' => 'ou cliquez pour parcourir vos fichiers',
|
||||
'bs_csv_err_type' => 'Erreur : Seuls les fichiers .csv sont autorisés.',
|
||||
'bs_csv_file_ok' => 'Fichier chargé',
|
||||
'bs_csv_preview_title' => 'Aperçu de vos colonnes',
|
||||
'bs_csv_empty_col' => 'Vide',
|
||||
'bs_csv_no_data' => 'Aucune ligne de transaction trouvée dans ce fichier.',
|
||||
|
||||
// --- BUDGET : SUIVI ---
|
||||
'bud_rem_school' => 'École (Reste estimé)',
|
||||
@@ -453,6 +652,25 @@ return [
|
||||
'bud_to_define' => "À définir",
|
||||
'bud_is_charge' => "Est une charge fixe...",
|
||||
'bud_is_income' => "Est un revenu...",
|
||||
'bud_edit_title' => 'Modifier la transaction',
|
||||
'bud_to_define_js' => 'à définir',
|
||||
'bud_select_beneficiary' => 'Choisir un revenu attendu',
|
||||
'bud_bar_actual' => 'Actuel',
|
||||
|
||||
// ONBOARDING BUDGET
|
||||
'bs_onb_title' => 'Bienvenue ! Configurons votre budget :',
|
||||
'bs_onb_desc' => 'Complétez ces étapes pour que l\'application puisse calculer votre Reste à Vivre :',
|
||||
'bs_onb_cat_link' => 'Catégories',
|
||||
'bs_onb_cat_desc' => 'Créez au moins une catégorie de dépenses.',
|
||||
'bs_onb_acc_link' => 'Comptes Bancaires',
|
||||
'bs_onb_acc_desc' => 'Ajoutez au moins un compte courant ou épargne.',
|
||||
'bs_onb_sal_link' => 'Salaires',
|
||||
'bs_onb_sal_desc' => 'Renseignez les rentrées d\'argent de l\'année en cours.',
|
||||
'bs_onb_items_link' => 'Lignes de Budget',
|
||||
'bs_onb_items_desc' => 'Rendez-vous dans l\'onglet Récapitulatif pour lister vos charges fixes (Loyer, Assurances...).',
|
||||
'bs_rule_search_ph' => '🔍 Rechercher un mot-clé (ex: AUCHAN)...',
|
||||
'error_loading_settings' => 'Erreur lors du chargement des paramètres',
|
||||
'settings' => 'Paramètres',
|
||||
|
||||
// --- BUDGET : RECAP ---
|
||||
'bud_recap_monthly_title' => 'Récapitulatif Mensuel',
|
||||
@@ -495,6 +713,10 @@ return [
|
||||
'bud_reg_none' => 'Aucune',
|
||||
'bud_recap_confirm_delete' => 'Voulez-vous vraiment supprimer cet élément ?',
|
||||
'bud_err_delete' => 'Erreur lors de la suppression.',
|
||||
'bud_prev_monthly_target' => 'Montant cible / Mensualité (€)',
|
||||
'bud_prev_transfer_dest' => 'Destination du virement',
|
||||
'bud_prev_none' => 'Aucune destination (Charge standard)',
|
||||
|
||||
|
||||
// --- BUDGET : EPARGNE ---
|
||||
'bud_sav_add_month' => 'Saisir un mois',
|
||||
@@ -525,6 +747,68 @@ return [
|
||||
'bud_sav_prompt_duplicate' => "Voulez-vous copier les données de %s vers %t1 ?\n\nSaisissez le nouveau TOTAL en banque (€) pour %t2 :",
|
||||
'bud_err_server' => 'Erreur serveur : ',
|
||||
'bud_err_network_dup' => 'Erreur réseau lors de la duplication.',
|
||||
'budget_tab_kids' => 'Enfants 👶',
|
||||
'bud_sav_add_one_month' => '+1 Mois',
|
||||
'bud_sav_add_month' => 'Ajouter un mois',
|
||||
'bud_sav_no_data' => 'Aucune donnée d\'épargne pour %s.',
|
||||
'bud_sav_post_month' => 'Poste / Mois',
|
||||
'bud_sav_from_date' => 'Dès le %s',
|
||||
'bud_sav_edit_modal' => 'Éditer ce mois',
|
||||
'bud_sav_delete_month' => 'Supprimer ce mois',
|
||||
'bud_sav_total_bank' => 'TOTAL BANQUE',
|
||||
'bud_sav_extra' => 'NON ALLOUÉ (EXTRAS)',
|
||||
'bud_sav_modal_title_add' => 'Ajouter un mois',
|
||||
'bud_sav_modal_title_edit' => 'Éditer',
|
||||
'bud_sav_month_concerned' => 'Mois concerné',
|
||||
'bud_sav_total_bank_eur' => 'Total en Banque (€)',
|
||||
'bud_sav_ventilation' => 'Ventilation des comptes',
|
||||
'bud_sav_adj_help' => 'Saisissez les ajustements (+/-) pour recalculer automatiquement.',
|
||||
'bud_sav_add_line' => 'Ajouter une ligne',
|
||||
'bud_category' => 'Catégorie',
|
||||
'bud_sav_current' => 'Actuel',
|
||||
'bud_sav_adjust' => 'Ajustement',
|
||||
'bud_sav_new' => 'Nouveau',
|
||||
'bud_sav_sum_mode_title' => 'Activer/Désactiver la calculatrice',
|
||||
'bud_sav_selection' => 'Sélection :',
|
||||
'bud_sav_ph_name' => 'Nom du compte',
|
||||
|
||||
|
||||
// --- BUDGET : PREVISION ---
|
||||
|
||||
'budget_provisions_title' => 'Provisions & Optimisation de Trésorerie',
|
||||
'btn_optimize_cashflow' => 'Optimiser mes Économies',
|
||||
'add_new_provision' => 'Anticiper une grosse dépense à venir',
|
||||
'provision_label' => 'Libellé de la dépense',
|
||||
'provision_placeholder_wood' => 'Ex: Commande bois de chauffage, Révision voiture...',
|
||||
'amount' => 'Montant',
|
||||
'expected_date' => 'Date prévue',
|
||||
'btn_add' => 'Ajouter',
|
||||
'upcoming_provisions_list' => 'Dépenses prévues au calendrier',
|
||||
'optimization_assistant_title' => 'Assistant d\'Optimisation des Quinzaines',
|
||||
'error_generic' => 'Une erreur est survenue',
|
||||
'success_add_provision' => 'Dépense prévisionnelle ajoutée !',
|
||||
'no_provisions' => 'Aucune dépense prévisionnelle enregistrée pour le moment.',
|
||||
'btn_delete' => 'Supprimer',
|
||||
'savings_this_month' => 'Économies globales à placer ce mois-ci',
|
||||
'calculate' => 'Calculer la stratégie optimale',
|
||||
'date_past_warning' => 'La date sélectionnée est déjà passée !',
|
||||
'budget_tab_provisions' => 'Provisions',
|
||||
'budget_opti_base' => 'Base',
|
||||
'budget_opti_debt' => 'Dette compensée',
|
||||
'budget_opti_no_config' => 'Aucune configuration de salaire trouvée pour cette année.',
|
||||
'budget_opti_recap_apports' => 'Résumé des apports (Clearing) :',
|
||||
'budget_opti_total_disp' => 'Total disponible pour l\'optimisation :',
|
||||
'budget_opti_must_pay' => 'doit verser',
|
||||
|
||||
|
||||
// JS & Erreurs
|
||||
'bud_sav_confirm_delete_month' => 'Voulez-vous vraiment supprimer toutes les données de %m pour %o ?',
|
||||
'bud_sav_prompt_duplicate' => "Voulez-vous dupliquer les données de %s vers %t1 ?\n\nSaisissez le nouveau TOTAL en banque (€) pour %t2 :",
|
||||
'bud_err_tech' => 'Une erreur technique est survenue.',
|
||||
'bud_err_server' => 'Erreur serveur : ',
|
||||
'bud_err_network_dup' => 'Erreur réseau lors de la duplication.',
|
||||
'bud_sav_saving' => 'Sauvegarde en cours...',
|
||||
'bud_err_delete' => 'Erreur lors de la suppression.',
|
||||
|
||||
// --- BUDGET : PREVISIONNEL ---
|
||||
'bud_prev_incomes' => 'Revenus',
|
||||
@@ -563,50 +847,93 @@ return [
|
||||
'bud_sav_modal_title_add' => 'Saisir un mois',
|
||||
'bud_sav_ph_name' => 'Nom (ex: Vacances)',
|
||||
'bud_prev_label_name' => 'Nom',
|
||||
'bud_adv_title' => 'Avances & Tricount Familial',
|
||||
'bud_adv_add' => 'Ajouter une avance',
|
||||
'bud_adv_edit' => 'Modifier l\'avance',
|
||||
'bud_adv_who_paid' => 'Qui a payé ?',
|
||||
'bud_adv_payer' => 'Payeur',
|
||||
'bud_adv_has_advanced' => '%s a avancé :',
|
||||
'bud_adv_cc_label' => 'sur le compte commun',
|
||||
'bud_adv_livret_label' => 'depuis son livret d\'épargne',
|
||||
'bud_adv_cc_balance_title' => 'Équilibre du Compte Commun',
|
||||
'bud_adv_owed_to' => '<strong>%s €</strong> sont dus à <strong>%s</strong>',
|
||||
'bud_adv_balanced' => 'Les comptes sont parfaitement équilibrés !',
|
||||
'bud_adv_saved_badge' => 'Épargne',
|
||||
'bud_adv_already_saved' => 'Cet argent provient d\'un livret d\'épargne',
|
||||
'bud_adv_ph_desc' => 'Ex: Courses, Facture Électricité...',
|
||||
'bud_adv_confirm_resolve' => 'Confirmer le remboursement complet de cette avance ?',
|
||||
'bud_adv_confirm_delete' => 'Supprimer définitivement cette avance ?',
|
||||
'bud_prev_target_ph' => 'Ex: 150',
|
||||
'bud_prev_transfer_to' => 'Vers',
|
||||
|
||||
// ==========================================
|
||||
// CADEAUX (GIFTS)
|
||||
// MODULE : CADEAUX (gift-list)
|
||||
// ==========================================
|
||||
'gift_page_title' => 'HouseHub - Liste de cadeaux',
|
||||
'gift_occ_tio' => 'Tió',
|
||||
'gift_occ_noel' => 'Noël',
|
||||
'gift_occ_rois' => 'Rois mages',
|
||||
'gift_occ_anniv' => 'Anniversaire',
|
||||
'gift_occ_sant' => 'Saint',
|
||||
'gift_main_title' => 'Liste de cadeaux %s',
|
||||
'gift_aria_change_view' => 'Changer la vue',
|
||||
'gift_view_nadal' => 'Noël',
|
||||
'gift_view_anniv' => 'Anniversaires',
|
||||
'gift_view_by_party' => 'Vue par fête',
|
||||
'gift_no_gifts' => 'Aucun cadeau enregistré pour %s dans cette vue.',
|
||||
'gift_add_gift' => 'Ajouter un cadeau',
|
||||
'gift_paid_by' => 'payé par %s',
|
||||
'gift_summary_title' => 'Résumé du budget',
|
||||
'gift_col_adult' => 'Adulte',
|
||||
'gift_col_child' => 'Enfant',
|
||||
'gift_col_party' => 'Fête',
|
||||
'gift_debtor' => 'Débiteur',
|
||||
'gift_creditor' => 'Créancier',
|
||||
'gift_liquidations' => 'Tricount',
|
||||
'gift_no_debt' => 'Aucune dette en cours.',
|
||||
'gift_owes' => 'doit',
|
||||
'gift_to' => 'à',
|
||||
'gift_detailed_list' => 'Liste détaillée des cadeaux',
|
||||
'gift_col_gift' => 'Cadeau',
|
||||
'gift_col_link' => 'Lien',
|
||||
'gift_modal_title_add' => 'Ajouter un cadeau pour %s',
|
||||
'gift_modal_title_edit' => 'Modifier le cadeau',
|
||||
'gift_modal_payer' => 'Payé par',
|
||||
'gift_modal_gift_name' => 'Nom du cadeau',
|
||||
'gift_modal_ph_name' => 'ex: Lego Star Wars',
|
||||
'gift_modal_price' => 'Prix (€)',
|
||||
'gift_modal_link' => 'Lien (optionnel)',
|
||||
'gift_confirm_delete' => 'Voulez-vous vraiment supprimer ce cadeau ?',
|
||||
|
||||
// 1. Titres et en-têtes
|
||||
'gift_page_title' => 'Liste des Cadeaux',
|
||||
'gift_main_title' => 'Cadeaux de %s', // %s sera remplacé par l'année (ex: Cadeaux de 2026)
|
||||
'settings' => 'Paramètres',
|
||||
|
||||
// 2. Filtres
|
||||
'gift_filter_all_children' => 'Tous les enfants',
|
||||
'gift_filter_all_adults' => 'Tous les adultes',
|
||||
'gift_empty_state_no_gifts' => 'Aucun cadeau pour le moment.',
|
||||
'gift_empty_state_no_filter' => 'Aucun cadeau ne correspond au filtre.',
|
||||
|
||||
// 3. Contenu et états vides
|
||||
'gift_add_gift' => 'Ajouter',
|
||||
'gift_empty_state_no_gifts' => 'Aucun cadeau enregistré pour cette fête.',
|
||||
'gift_empty_state_no_filter' => 'Aucun cadeau ne correspond à vos filtres.',
|
||||
'gift_paid_by' => 'Payé par %s', // %s sera remplacé par le nom du payeur
|
||||
|
||||
// 4. Section Tricount (Bilan & Remboursements)
|
||||
'gift_liquidations' => 'Bilan & Remboursements',
|
||||
'gift_no_debt' => 'Tout est à jour, aucune dette !',
|
||||
'gift_owes' => 'doit',
|
||||
'gift_view_matrix' => 'Voir la matrice détaillée',
|
||||
'gift_debtor' => 'Débiteur',
|
||||
'gift_creditor' => 'Créancier',
|
||||
|
||||
// 5. Modale : Ajout/Modification de Cadeau
|
||||
'gift_col_adult' => 'Pour le compte de',
|
||||
'gift_modal_payer' => 'Payé par',
|
||||
'gift_modal_gift_name' => 'Description du cadeau',
|
||||
'gift_modal_price' => 'Prix estimé / payé (€)',
|
||||
'gift_modal_link' => 'Lien du produit (URL)',
|
||||
'gift_modal_title_add' => 'Ajouter pour %s', // JS : %s = nom de l'enfant
|
||||
'gift_modal_title_edit' => 'Modifier le cadeau', // JS
|
||||
'gift_confirm_delete' => 'Voulez-vous vraiment supprimer ce cadeau ?', // JS
|
||||
|
||||
// 6. Modale : Paramètres des Fêtes (Nouveau !)
|
||||
'gift_settings_title' => 'Configuration des Fêtes',
|
||||
'gift_settings_add_title' => '+ Ajouter une occasion',
|
||||
'gift_settings_name_placeholder' => 'Nom (ex: Saint Valentin)',
|
||||
'gift_settings_date_placeholder' => 'MM-JJ',
|
||||
'gift_settings_date_help' => 'La date (MM-JJ) est optionnelle. Laissez vide pour une fête mobile.',
|
||||
'gift_settings_empty' => 'Aucune fête configurée.', // JS
|
||||
'gift_settings_active' => 'Actif', // JS
|
||||
|
||||
// 7. Boutons génériques (si tu ne les as pas déjà en global)
|
||||
'btn_cancel' => 'Annuler',
|
||||
'btn_save' => 'Enregistrer',
|
||||
'btn_add' => 'Ajouter',
|
||||
'loading' => 'Chargement...',
|
||||
|
||||
// --- MODULE CADEAUX : SETTINGS ---
|
||||
'gift_settings' => 'Configuration',
|
||||
'gift_tab_occasions' => 'Fêtes & Occasions',
|
||||
'gift_tab_proches' => 'Proches (Hors foyer)',
|
||||
'gift_settings_icon' => 'Icône',
|
||||
'gift_settings_date' => 'Date (MM-JJ)',
|
||||
'gift_settings_add_occ' => 'Ajouter une fête',
|
||||
'gift_settings_add_proche'=> 'Ajouter un proche',
|
||||
'gift_role_proche_adulte'=> 'Adulte (Oncle, ami...)',
|
||||
'gift_role_proche_enfant'=> 'Enfant (Neveu, nièce...)',
|
||||
'gift_ph_proche_name' => 'Prénom',
|
||||
'gift_role_label' => 'Rôle',
|
||||
'gift_settings_saved' => 'Paramètres enregistrés !',
|
||||
'gift_settings_edit_occ' => 'Modifier la fête',
|
||||
'gift_settings_edit_proche' => 'Modifier le proche',
|
||||
|
||||
// ==========================================
|
||||
// GARAGE MANAGER
|
||||
// ==========================================
|
||||
|
||||
+18
-2
@@ -1,9 +1,23 @@
|
||||
<?php
|
||||
// includes/meta_db.php
|
||||
// Connexion à la base meta (gestion des familles et utilisateurs)
|
||||
$meta_host = getenv('DB_HOST') ?: 'househub-db';
|
||||
$meta_db = 'househub_meta';
|
||||
|
||||
$envHost = getenv('DB_HOST');
|
||||
|
||||
// Détection stricte : Docker injecte toujours 'househub-db' via le docker-compose
|
||||
if ($envHost === 'househub-db') {
|
||||
// Environnement Docker (Ton collègue ou la Prod)
|
||||
$meta_host = 'househub-db';
|
||||
$meta_user = getenv('DB_USER') ?: 'househub';
|
||||
$meta_pass = getenv('DB_PASS') ?: 'changeme';
|
||||
} else {
|
||||
// Environnement Local XAMPP (Toi)
|
||||
$meta_host = '127.0.0.1'; // Plus stable que 'localhost' sous XAMPP
|
||||
$meta_user = 'root';
|
||||
$meta_pass = '';
|
||||
}
|
||||
|
||||
$meta_db = 'househub_meta';
|
||||
|
||||
try {
|
||||
$meta_pdo = new PDO(
|
||||
@@ -14,8 +28,10 @@ try {
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_general_ci",
|
||||
]
|
||||
);
|
||||
} catch (\PDOException $e) {
|
||||
die(json_encode(['error' => 'Meta DB unavailable: ' . $e->getMessage()]));
|
||||
}
|
||||
?>
|
||||
@@ -18,14 +18,6 @@ require __DIR__ . '/header.php';
|
||||
<button class="liste-tab-add" id="btn-add-list" title="<?= htmlspecialchars(tr('liste_new_list')) ?>">+</button>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire de création de liste inline -->
|
||||
<div class="liste-new-form hidden" id="liste-new-form">
|
||||
<input type="text" id="new-list-name" maxlength="100"
|
||||
placeholder="<?= htmlspecialchars(tr('liste_list_name_placeholder')) ?>" autocomplete="off">
|
||||
<button id="btn-confirm-new-list"><?= htmlspecialchars(tr('liste_create')) ?></button>
|
||||
<button id="btn-cancel-new-list"><?= htmlspecialchars(tr('cancel')) ?></button>
|
||||
</div>
|
||||
|
||||
<!-- Corps de la liste active -->
|
||||
<div class="liste-body" id="liste-body">
|
||||
|
||||
|
||||
+31
-29
@@ -1,46 +1,48 @@
|
||||
<?php
|
||||
// Script de migration pour mettre à jour TOUTES les bases de données familiales
|
||||
require_once __DIR__ . '/includes/meta_db.php';
|
||||
|
||||
echo "<h1>🚀 Début de la migration...</h1>";
|
||||
// Récupération des identifiants de BDD depuis l'environnement (fallback par défaut)
|
||||
$db_host = getenv('DB_HOST') ?: 'househub-db';
|
||||
$db_user = getenv('DB_USER') ?: 'househub';
|
||||
$db_pass = getenv('DB_PASS') ?: 'changeme';
|
||||
|
||||
// 1. Récupérer toutes les bases de données des familles
|
||||
$stmt = $meta_pdo->query("SELECT name, db_name FROM families WHERE db_name != ''");
|
||||
$families = $stmt->fetchAll();
|
||||
|
||||
$host = getenv('DB_HOST') ?: 'househub-db';
|
||||
$user = getenv('DB_USER') ?: 'househub';
|
||||
$pass = getenv('DB_PASS') ?: 'changeme';
|
||||
|
||||
// 2. Boucler sur chaque famille
|
||||
foreach ($families as $f) {
|
||||
$db_name = $f['db_name'];
|
||||
echo "Mise à jour de la famille <strong>{$f['name']}</strong> (Base : $db_name) ... ";
|
||||
echo "<h1>🛠️ HouseHub - Correction AUTO_INCREMENT Voyages</h1>";
|
||||
|
||||
try {
|
||||
$fam_pdo = new PDO("mysql:host=$host;dbname=$db_name;charset=utf8mb4", $user, $pass, [
|
||||
// On récupère toutes les familles actives
|
||||
$stmt = $meta_pdo->query("SELECT db_name, name FROM families");
|
||||
$families = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($families as $f) {
|
||||
$dbName = $f['db_name'];
|
||||
echo "<h3>Famille : {$f['name']} ($dbName)</h3><ul>";
|
||||
|
||||
try {
|
||||
$pdo = new PDO("mysql:host=$db_host;dbname=$dbName;charset=utf8mb4", $db_user, $db_pass, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
||||
]);
|
||||
|
||||
// 3. Injecter la nouvelle table
|
||||
$sql = "
|
||||
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;
|
||||
// 1. On vérifie si la clé primaire existe déjà pour éviter de faire planter le script
|
||||
$hasPrimaryKey = $pdo->query("SHOW KEYS FROM pf_holidays WHERE Key_name = 'PRIMARY'")->fetch();
|
||||
|
||||
INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '€', 'C');
|
||||
";
|
||||
if (!$hasPrimaryKey) {
|
||||
$pdo->exec("ALTER TABLE pf_holidays ADD PRIMARY KEY (id)");
|
||||
echo "<li>✅ Clé primaire restaurée sur la colonne `id`.</li>";
|
||||
}
|
||||
|
||||
$fam_pdo->exec($sql);
|
||||
echo "<span style='color:green'>✅ OK</span><br>";
|
||||
// 2. On restaure la propriété AUTO_INCREMENT
|
||||
$pdo->exec("ALTER TABLE pf_holidays MODIFY id INT(11) NOT NULL AUTO_INCREMENT");
|
||||
echo "<li>✅ Propriété AUTO_INCREMENT restaurée.</li>";
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
echo "<span style='color:red'>❌ Erreur : " . $e->getMessage() . "</span><br>";
|
||||
// On attrape l'erreur si la modif a déjà été faite
|
||||
echo "<li>ℹ️ Action ignorée ou table déjà à jour : " . $e->getMessage() . "</li>";
|
||||
}
|
||||
echo "</ul>";
|
||||
}
|
||||
echo "<h2>🚀 Correction terminée ! Tu peux réessayer de créer un voyage.</h2>";
|
||||
|
||||
echo "<h2>🎉 Migration terminée ! Tu peux supprimer ce fichier.</h2>";
|
||||
} catch (Exception $e) {
|
||||
die("Erreur fatale de connexion à la Meta DB : " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
// migrate_attachments.php
|
||||
|
||||
// 1. Inclusion de ta connexion BDD principale (Ajuste le chemin si besoin)
|
||||
require __DIR__ . '/includes/db.php';
|
||||
|
||||
echo "<pre>🚀 Lancement de la migration multi-bases...\n\n";
|
||||
|
||||
try {
|
||||
// 2. Récupérer toutes les bases de données qui commencent par "househub_f"
|
||||
$stmt = $pdo->query("SHOW DATABASES LIKE 'househub_f%'");
|
||||
$databases = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if (empty($databases)) {
|
||||
die("❌ Aucune base de données familiale trouvée (Format househub_f1, househub_f2...).\n");
|
||||
}
|
||||
|
||||
$successCount = 0;
|
||||
|
||||
foreach ($databases as $dbName) {
|
||||
// Sécurité : on vérifie le format exact (househub_f suivi de chiffres)
|
||||
if (preg_match('/^househub_f\d+$/', $dbName)) {
|
||||
echo "⚙️ Mise à jour de la base : <strong>$dbName</strong>... ";
|
||||
|
||||
// On bascule la connexion sur la base de la famille
|
||||
$pdo->exec("USE `$dbName`");
|
||||
|
||||
// 3. Création de la table
|
||||
$sql = "CREATE TABLE IF NOT EXISTS pf_holidays_attachments (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
holiday_id INT NOT NULL,
|
||||
item_id INT DEFAULT NULL,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
file_path VARCHAR(255) NOT NULL,
|
||||
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_holiday_attachment FOREIGN KEY (holiday_id) REFERENCES pf_holidays(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;";
|
||||
|
||||
$pdo->exec($sql);
|
||||
echo "✅ OK\n";
|
||||
$successCount++;
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n🎉 Migration terminée avec succès sur $successCount base(s) familiale(s) !</pre>";
|
||||
|
||||
} catch (PDOException $e) {
|
||||
die("\n❌ Erreur SQL pendant la migration : " . $e->getMessage() . "</pre>");
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
// migrate_categories_order.php
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION DE CONNEXION (À ADAPTER)
|
||||
// ============================================================================
|
||||
require __DIR__ . '/includes/db.php';
|
||||
|
||||
|
||||
echo "<h2>🚀 Migration : Ajout de l'ordre des catégories</h2>";
|
||||
|
||||
try {
|
||||
// Connexion au serveur MySQL (sans spécifier de base de données particulière)
|
||||
$pdo = new PDO("mysql:host=$host;charset=utf8mb4", $user, $pass);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// 1. Trouver toutes les bases de données de la famille HouseHub
|
||||
$stmt = $pdo->query("SHOW DATABASES LIKE 'househub_f%'");
|
||||
$databases = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if (empty($databases)) {
|
||||
echo "<p style='color: #ef4444;'>❌ Aucune base de données correspondant à 'househub_f*' n'a été trouvée.</p>";
|
||||
exit;
|
||||
}
|
||||
|
||||
echo "<p>🔍 <strong>" . count($databases) . "</strong> base(s) de données trouvée(s).</p>";
|
||||
|
||||
// 2. Boucler sur chaque base de données
|
||||
foreach ($databases as $db) {
|
||||
echo "<hr><div style='padding: 10px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; margin-bottom: 10px;'>";
|
||||
echo "<h4 style='margin-top: 0;'>📂 Traitement de la base : <strong>$db</strong></h4>";
|
||||
|
||||
// Sélectionner la base de données active
|
||||
$pdo->exec("USE `$db`");
|
||||
|
||||
// Vérifier si la table pf_budget_categories existe dans cette DB
|
||||
$tableExists = $pdo->query("SHOW TABLES LIKE 'pf_budget_categories'")->rowCount() > 0;
|
||||
|
||||
if ($tableExists) {
|
||||
// Vérifier si la colonne sort_order existe déjà (sécurité)
|
||||
$columnExists = $pdo->query("SHOW COLUMNS FROM pf_budget_categories LIKE 'sort_order'")->rowCount() > 0;
|
||||
|
||||
if (!$columnExists) {
|
||||
// La colonne n'existe pas, on l'ajoute !
|
||||
$pdo->exec("ALTER TABLE pf_budget_categories ADD sort_order INT DEFAULT 0");
|
||||
echo "<span style='color: #10b981;'>✅ Colonne 'sort_order' ajoutée avec succès !</span>";
|
||||
} else {
|
||||
// La colonne est déjà là
|
||||
echo "<span style='color: #f59e0b;'>ℹ️ La colonne 'sort_order' existe déjà (Ignoré).</span>";
|
||||
}
|
||||
} else {
|
||||
echo "<span style='color: #ef4444;'>⚠️ La table 'pf_budget_categories' n'existe pas ici.</span>";
|
||||
}
|
||||
echo "</div>";
|
||||
}
|
||||
|
||||
echo "<h3>🎉 Migration terminée avec succès !</h3>";
|
||||
echo "<p style='color: #64748b;'><em>Tu peux maintenant supprimer ce fichier par mesure de sécurité.</em></p>";
|
||||
|
||||
} catch (PDOException $e) {
|
||||
echo "<h3 style='color: #ef4444;'>❌ Erreur fatale MySQL :</h3>";
|
||||
echo "<pre style='background: #fee2e2; padding: 15px; border-radius: 8px;'>" . $e->getMessage() . "</pre>";
|
||||
}
|
||||
?>
|
||||
+922
-70
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
// On charge l'authentification et l'init PDO de l'espace familial courant automatiquement via tes includes globaux
|
||||
require_once __DIR__ . '/../../../../includes/auth.php';
|
||||
require_once __DIR__ . '/../../../../includes/db.php';
|
||||
require_once __DIR__ . '/../../../../includes/i18n.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$amount = floatval($_POST['amount'] ?? 0);
|
||||
$expected_date = $_POST['expected_date'] ?? '';
|
||||
|
||||
if (empty($title) || $amount <= 0 || empty($expected_date)) {
|
||||
echo json_encode(['success' => false, 'message' => tr('error_generic')]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// Grâce au multi-tenant par BDD, $pdo pointe déjà sur la base de la famille connectée
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_expected_expenses (title, amount, expected_date) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$title, $amount, $expected_date]);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => tr('success_add_provision')]);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/../../../../includes/auth.php';
|
||||
require_once __DIR__ . '/../../../../includes/db.php';
|
||||
|
||||
$id = intval($_POST['id'] ?? 0);
|
||||
|
||||
if (!$id) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid ID']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("DELETE FROM pf_expected_expenses WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/../../../../includes/auth.php';
|
||||
require_once __DIR__ . '/../../../../includes/db.php';
|
||||
require_once __DIR__ . '/../../../../includes/i18n.php';
|
||||
|
||||
try {
|
||||
// Récupérer les provisions non payées, classées par date
|
||||
$stmt = $pdo->query("SELECT id, title, amount, expected_date FROM pf_expected_expenses WHERE is_paid = 0 ORDER BY expected_date ASC");
|
||||
$provisions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($provisions)) {
|
||||
$html = '<p class="pf-text-muted">' . tr('no_provisions') . '</p>';
|
||||
} else {
|
||||
$html = '<table class="pf-table" style="width:100%; border-collapse: collapse;">';
|
||||
$html .= '<thead style="border-bottom: 2px solid var(--border-color); text-align: left;">';
|
||||
$html .= '<tr><th>' . tr('provision_label') . '</th><th>' . tr('amount') . '</th><th>' . tr('expected_date') . '</th><th style="text-align:right;">Actions</th></tr>';
|
||||
$html .= '</thead><tbody>';
|
||||
|
||||
foreach ($provisions as $p) {
|
||||
$dateFormated = date('d/m/Y', strtotime($p['expected_date']));
|
||||
$html .= '<tr style="border-bottom: 1px solid var(--border-color); height: 45px;">';
|
||||
$html .= '<td>' . htmlspecialchars($p['title']) . '</td>';
|
||||
$html .= '<td>' . number_format($p['amount'], 2, ',', ' ') . ' €</td>';
|
||||
$html .= '<td>' . $dateFormated . '</td>';
|
||||
$html .= '<td style="text-align:right;"><button class="pf-btn pf-btn-danger btn-delete-provision" data-id="' . $p['id'] . '">' . tr('btn_delete') . '</button></td>';
|
||||
$html .= '</tr>';
|
||||
}
|
||||
$html .= '</tbody></table>';
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'html' => $html]);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -6,30 +6,25 @@ require_login();
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
// --- ACTION : SAUVEGARDER (AJOUT OU MODIF) ---
|
||||
// --- ACTION : SAUVEGARDER (AJOUT OU MODIF DU BUDGET) ---
|
||||
if ($action === 'save') {
|
||||
$id = $_POST['id'] ?? '';
|
||||
$name = $_POST['name'];
|
||||
$category = $_POST['category'];
|
||||
$type = $_POST['type'];
|
||||
$payment_day = empty($_POST['payment_day']) ? null : (int)$_POST['payment_day'];
|
||||
$is_estimate = $_POST['is_estimate'];
|
||||
$reg_month = $_POST['reg_month'];
|
||||
$keywords = $_POST['mapping_keywords'] ?? '';
|
||||
try {
|
||||
// 1. Sécurisation des données entrantes
|
||||
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$category = $_POST['category'] ?? '';
|
||||
$type = $_POST['type'] ?? 'Mensuel';
|
||||
$payment_day = !empty($_POST['payment_day']) ? (int)$_POST['payment_day'] : null;
|
||||
$is_estimate = isset($_POST['is_estimate']) ? (int)$_POST['is_estimate'] : 0;
|
||||
$reg_month = !empty($_POST['reg_month']) ? trim($_POST['reg_month']) : null;
|
||||
$keywords = trim($_POST['mapping_keywords'] ?? '');
|
||||
$holiday_id = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
|
||||
|
||||
// ==========================================
|
||||
// NOUVELLE NORME COMPTABLE : Dépense = Négatif
|
||||
// ==========================================
|
||||
// On récupère le montant en valeur absolue (pour éviter les erreurs si l'utilisateur a tapé un '-')
|
||||
$amount = abs((float)$_POST['amount']);
|
||||
|
||||
// Si c'est un frais, on l'enregistre en négatif
|
||||
if ($category === 'expense') {
|
||||
$amount = -$amount;
|
||||
}
|
||||
// ==========================================
|
||||
// 2. Gestion du montant (toujours stocker les dépenses en négatif)
|
||||
$amount = abs((float)($_POST['amount'] ?? 0));
|
||||
$amount = -$amount; // On force le négatif car le modal Recap ne gère que des charges
|
||||
|
||||
// 3. Exécution de la requête
|
||||
if ($id) {
|
||||
// UPDATE
|
||||
$stmt = $pdo->prepare("UPDATE pf_budget_items SET name=?, amount=?, category=?, type=?, payment_day=?, is_estimate=?, reg_month=?, mapping_keywords=?, holiday_id=? WHERE id=?");
|
||||
@@ -40,11 +35,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$stmt->execute([$name, $amount, $category, $type, $payment_day, $is_estimate, $reg_month, $keywords, $holiday_id]);
|
||||
}
|
||||
|
||||
// Redirection
|
||||
// 4. Réponse appropriée (JSON si appel via JS fetch, Redirection sinon)
|
||||
if (!empty($_POST['ajax'])) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
} else {
|
||||
header('Location: /budget.php?tab=recap');
|
||||
exit;
|
||||
}
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// En cas d'erreur BDD (ex: contrainte de clé étrangère, colonne manquante)
|
||||
if (!empty($_POST['ajax'])) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => 'Erreur BDD : ' . $e->getMessage()]);
|
||||
exit;
|
||||
}
|
||||
die("Erreur fatale : " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// --- ACTION : COCHER/DÉCOCHER RAPIDE (VIA JS FETCH) ---
|
||||
if ($action === 'toggle-check') {
|
||||
$id = $_POST['id'];
|
||||
@@ -59,7 +70,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- ACTION : SUPPRIMER ---
|
||||
// --- ACTION : SUPPRIMER UNE LIGNE DU BUDGET ---
|
||||
if ($action === 'delete') {
|
||||
$id = $_POST['id'];
|
||||
$pdo->prepare("DELETE FROM pf_budget_items WHERE id = ?")->execute([$id]);
|
||||
@@ -84,7 +95,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
// --- ACTION : SAUVEGARDER UNE DÉPENSE MANUELLE (FETCH) ---
|
||||
if ($action === 'save_expense_manual') {
|
||||
header('Content-Type: application/json'); // On force le retour JSON pour notre script JS
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
$id = !empty($_POST['expense_id']) ? (int)$_POST['expense_id'] : null;
|
||||
@@ -92,26 +103,31 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$amount = floatval($_POST['amount'] ?? 0);
|
||||
$date = $_POST['date'] ?? date('Y-m-d');
|
||||
|
||||
// 🛡️ SÉCURITÉ : Format attendu pour gestion_month (qui vient de input type="month") : YYYY-MM.
|
||||
// Format attendu : YYYY-MM
|
||||
$gestionMonthRaw = $_POST['gestion_month'] ?? '';
|
||||
|
||||
// Si la valeur est vide ou '0000-00-00'
|
||||
// Si la valeur est vide ou invalide
|
||||
if (empty($gestionMonthRaw) || strpos($gestionMonthRaw, '0000') !== false) {
|
||||
// On force le mois de gestion au 1er jour de la date de la dépense
|
||||
$gestionMonth = date('Y-m-01', strtotime($date));
|
||||
} else {
|
||||
// Si la valeur vient d'un input "month" (ex: "2026-04"), on rajoute "-01"
|
||||
$gestionMonth = (strlen($gestionMonthRaw) === 7) ? $gestionMonthRaw . '-01' : $gestionMonthRaw;
|
||||
}
|
||||
|
||||
$label = trim($_POST['label'] ?? '');
|
||||
$rawItemId = $_POST['budget_item_id'] ?? '';
|
||||
$budgetItemId = null;
|
||||
$salaryId = null;
|
||||
$holidayId = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
|
||||
|
||||
if ($cat === 'School' && !empty($_POST['label_select'])) {
|
||||
$label = trim($_POST['label_select']);
|
||||
} elseif (($cat === 'Frais' || $cat === 'Income') && !empty($_POST['budget_item_id'])) {
|
||||
$budgetItemId = (int)$_POST['budget_item_id'];
|
||||
} elseif (!empty($rawItemId)) {
|
||||
// 🔥 On intercepte si c'est un Salaire (SAL_x) ou une Charge Fixe classique (x)
|
||||
if (strpos($rawItemId, 'SAL_') === 0) {
|
||||
$salaryId = (int)str_replace('SAL_', '', $rawItemId);
|
||||
} else {
|
||||
$budgetItemId = (int)$rawItemId;
|
||||
}
|
||||
}
|
||||
|
||||
// Vérification de sécurité
|
||||
@@ -124,14 +140,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$finalAmount = $is_credit ? abs($amount) : -abs($amount);
|
||||
|
||||
if ($id) {
|
||||
// UPDATE
|
||||
$pdo->prepare("UPDATE pf_expenses SET date_exp=?, gestion_month=?, category=?, label=?, amount=?, budget_item_id=?, holiday_id=? WHERE id=?")
|
||||
->execute([$date, $gestionMonth, $cat, $label, $finalAmount, $budgetItemId, $holidayId, $id]);
|
||||
// UPDATE (Ajout de salary_id)
|
||||
$pdo->prepare("UPDATE pf_expenses SET date_exp=?, gestion_month=?, category=?, label=?, amount=?, budget_item_id=?, holiday_id=?, salary_id=? WHERE id=?")
|
||||
->execute([$date, $gestionMonth, $cat, $label, $finalAmount, $budgetItemId, $holidayId, $salaryId, $id]);
|
||||
} else {
|
||||
// INSERT
|
||||
// INSERT (Ajout de salary_id)
|
||||
$uniqueRef = "MANUAL_" . uniqid();
|
||||
$pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
->execute([$date, $gestionMonth, $cat, $label, $finalAmount, $uniqueRef, $budgetItemId, $holidayId]);
|
||||
$pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id, salary_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
->execute([$date, $gestionMonth, $cat, $label, $finalAmount, $uniqueRef, $budgetItemId, $holidayId, $salaryId]);
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/../../../../includes/auth.php';
|
||||
require_once __DIR__ . '/../../../../includes/db.php';
|
||||
require_once __DIR__ . '/../../../../includes/i18n.php';
|
||||
|
||||
$savings_inputs = $_POST['savings'] ?? [];
|
||||
$current_year = date('Y');
|
||||
$current_month = date('m');
|
||||
|
||||
try {
|
||||
// 1. Récupération des dépenses prévues
|
||||
$stmtExpenses = $pdo->prepare("
|
||||
SELECT title, amount, expected_date
|
||||
FROM pf_expected_expenses
|
||||
WHERE is_paid = 0
|
||||
AND YEAR(expected_date) = ?
|
||||
AND MONTH(expected_date) = ?
|
||||
ORDER BY expected_date ASC
|
||||
");
|
||||
$stmtExpenses->execute([$current_year, $current_month]);
|
||||
$expenses = $stmtExpenses->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$q1_expenses_total = 0;
|
||||
$q2_expenses_total = 0;
|
||||
|
||||
$detail_html = '<ul style="padding-left: 1.2rem; margin-bottom: 1rem; color: var(--text-muted); font-size:0.9rem;">';
|
||||
foreach ($expenses as $e) {
|
||||
$day = intval(date('d', strtotime($e['expected_date'])));
|
||||
if ($day <= 15) {
|
||||
$q1_expenses_total += $e['amount'];
|
||||
} else {
|
||||
$q2_expenses_total += $e['amount'];
|
||||
}
|
||||
$detail_html .= '<li>' . htmlspecialchars($e['title']) . ' (' . number_format($e['amount'], 2, ',', ' ') . ' € le ' . date('d/m', strtotime($e['expected_date'])) . ')</li>';
|
||||
}
|
||||
$detail_html .= '</ul>';
|
||||
|
||||
$total_expenses = $q1_expenses_total + $q2_expenses_total;
|
||||
|
||||
// 2. Récupération des dettes
|
||||
$stmtDebts = $pdo->query("SELECT payer, SUM(amount) as total_debt FROM pf_advances WHERE is_resolved = 0 GROUP BY payer");
|
||||
$debts = $stmtDebts->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
// 3. NOUVELLE LOGIQUE : Clearing intelligent (Compensation partielle ou totale)
|
||||
$total_base_inputs = array_sum(array_map('floatval', $savings_inputs));
|
||||
$surplus_for_clearing = $total_base_inputs - $total_expenses;
|
||||
$monthly_savings = 0; // Le vrai total qui sera viré après déduction des dettes
|
||||
|
||||
$contributions_html = '<ul style="padding-left: 1.2rem; margin-bottom: 0.5rem; color: var(--text-main); font-size:0.9rem; line-height: 1.6;">';
|
||||
|
||||
foreach ($savings_inputs as $person => $amount) {
|
||||
$base_amount = floatval($amount);
|
||||
$debt = $debts[$person] ?? 0;
|
||||
|
||||
// Formatage bancaire propre pour les textes
|
||||
$base_fmt = number_format($base_amount, 2, ',', ' ');
|
||||
|
||||
if ($debt > 0) {
|
||||
if ($surplus_for_clearing > 0) {
|
||||
$compensation = min($debt, $base_amount, $surplus_for_clearing);
|
||||
$net_amount = $base_amount - $compensation;
|
||||
$surplus_for_clearing -= $compensation;
|
||||
$monthly_savings += $net_amount;
|
||||
|
||||
$comp_fmt = number_format($compensation, 2, ',', ' ');
|
||||
$net_fmt = number_format($net_amount, 2, ',', ' ');
|
||||
|
||||
$reste_dette = $debt - $compensation;
|
||||
$txt_reste = ($reste_dette > 0)
|
||||
? ' <span style="color:var(--danger); font-size:0.8rem; margin-left: 5px;">(Reste ' . number_format($reste_dette, 2, ',', ' ') . ' € de dette)</span>'
|
||||
: ' <span style="color:var(--success); font-weight:bold; font-size:0.85rem; margin-left: 5px;">(Dette soldée 🎉)</span>';
|
||||
|
||||
// Harmonisation en bleu (#2563eb) pour le montant net à verser
|
||||
$contributions_html .= '<li style="margin-bottom: 5px;"><strong>' . htmlspecialchars($person) . '</strong> : Base ' . $base_fmt . ' € − Remboursé ' . $comp_fmt . ' € = <strong style="color:#2563eb;">' . $net_fmt . ' € à verser</strong>' . $txt_reste . '</li>';
|
||||
} else {
|
||||
$monthly_savings += $base_amount;
|
||||
$contributions_html .= '<li style="margin-bottom: 5px;"><strong>' . htmlspecialchars($person) . '</strong> : <strong style="color:#2563eb;">' . $base_fmt . ' € à verser</strong> <span style="color:var(--danger); font-size:0.8rem; margin-left: 5px;">(Dette gelée, liquidités insuffisantes)</span></li>';
|
||||
}
|
||||
} else {
|
||||
$monthly_savings += $base_amount;
|
||||
$contributions_html .= '<li style="margin-bottom: 5px;"><strong>' . htmlspecialchars($person) . '</strong> : <strong style="color:#2563eb;">' . $base_fmt . ' € à verser</strong></li>';
|
||||
}
|
||||
}
|
||||
$contributions_html .= '</ul>';
|
||||
$contributions_html .= '<p style="color: var(--text-main); font-weight: bold; margin-top:5px; font-size:0.95rem;">Total net atterrissant sur les comptes : ' . number_format($monthly_savings, 2, ',', ' ') . ' €</p>';
|
||||
|
||||
if(empty($expenses)) {
|
||||
$detail_html = '<p style="color: var(--success); font-weight:bold; margin-bottom:1rem; font-size:0.9rem;">🎉 Aucune grosse dépense enregistrée sur ce mois.</p>';
|
||||
}
|
||||
|
||||
// --- MOTEUR DE RÈGLES DES QUINZAINES (Remplacement des ** par <strong>) ---
|
||||
$instructions = [];
|
||||
$remaining_to_allocate = $monthly_savings;
|
||||
|
||||
// Règle 1 : Première quinzaine
|
||||
if ($q1_expenses_total > 0) {
|
||||
if ($remaining_to_allocate >= $q1_expenses_total) {
|
||||
$remaining_to_allocate -= $q1_expenses_total;
|
||||
$instructions[] = "💼 <strong>Dès le 1er</strong> : Laissez <strong>" . number_format($q1_expenses_total, 2, ',', ' ') . " €</strong> sur le compte commun pour honorer les dépenses de la 1ère quinzaine.";
|
||||
} else {
|
||||
$deficit = $q1_expenses_total - $remaining_to_allocate;
|
||||
$remaining_to_allocate = 0;
|
||||
$instructions[] = "💼 <strong>Dès le 1er</strong> : Gardez l'intégralité des apports sur le compte commun.";
|
||||
$instructions[] = "⚠️ <strong>Retrait requis</strong> : Retirez <strong>" . number_format($deficit, 2, ',', ' ') . " €</strong> depuis le Livret A vers le commun pour couvrir la 1ère quinzaine.";
|
||||
}
|
||||
}
|
||||
|
||||
// Règle 2 : Deuxième quinzaine
|
||||
if ($q2_expenses_total > 0) {
|
||||
if ($remaining_to_allocate >= $q2_expenses_total) {
|
||||
$remaining_to_allocate -= $q2_expenses_total;
|
||||
$instructions[] = "📈 <strong>Dès le 1er</strong> : Placez <strong>" . number_format($q2_expenses_total, 2, ',', ' ') . " €</strong> sur le Livret A pour générer des intérêts sur la 1ère quinzaine.";
|
||||
$instructions[] = "🔄 <strong>Le 16 du mois</strong> : Transférez ces <strong>" . number_format($q2_expenses_total, 2, ',', ' ') . " €</strong> sur le compte commun pour payer la dépense.";
|
||||
} else {
|
||||
if ($remaining_to_allocate > 0) {
|
||||
$instructions[] = "📈 <strong>Dès le 1er</strong> : Placez le reste des apports (<strong>" . number_format($remaining_to_allocate, 2, ',', ' ') . " €</strong>) sur le Livret A.";
|
||||
$instructions[] = "🔄 <strong>Le 16 du mois</strong> : Retirez <strong>" . number_format($q2_expenses_total, 2, ',', ' ') . " €</strong> global du Livret A pour payer la fin de mois.";
|
||||
$remaining_to_allocate = 0;
|
||||
} else {
|
||||
$instructions[] = "🔄 <strong>Le 16 du mois</strong> : Retirez <strong>" . number_format($q2_expenses_total, 2, ',', ' ') . " €</strong> du Livret A vers le commun. <em>Ne le faites pas avant le 16 !</em>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Solde résiduel long terme
|
||||
if ($remaining_to_allocate > 0) {
|
||||
$instructions[] = "💰 <strong>Épargne stable</strong> : Placez les <strong>" . number_format($remaining_to_allocate, 2, ',', ' ') . " €</strong> restants sur votre Livret A dès le 1er.";
|
||||
}
|
||||
|
||||
// --- CONSTITUTION DU RENDU HTML FINAL ---
|
||||
$html = '<div style="border-top: 1px dashed var(--border-color); padding-top:0.8rem;">';
|
||||
|
||||
// Titre de l'état du Clearing simplifié et clair
|
||||
$html .= '<h4 style="margin:0 0 0.4rem 0; font-size:0.95rem;">🤝 Résumé des apports et remboursements internes :</h4>';
|
||||
$html .= '<div style="background: var(--bg-main); padding: 8px; border-radius: 6px; margin-bottom: 1rem; border: 1px solid var(--border-color);">';
|
||||
$html .= $contributions_html;
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<h4 style="margin:0 0 0.4rem 0; font-size:0.95rem;">Analyse des dépenses du mois :</h4>';
|
||||
$html .= $detail_html;
|
||||
|
||||
$html .= '<div style="background: var(--bg-main); padding: 8px; border-radius: 6px; border-left: 4px solid var(--accent-color);">';
|
||||
$html .= '<h4 style="margin:0 0 0.5rem 0; color: var(--accent-color); font-size:0.95rem;">📋 Plan d\'action recommandé :</h4>';
|
||||
|
||||
foreach ($instructions as $ins) {
|
||||
$html .= '<p style="margin:0 0 0.4rem 0; font-size: 0.9rem; line-height: 1.4;">' . $ins . '</p>';
|
||||
}
|
||||
|
||||
$html .= '</div></div>';
|
||||
|
||||
echo json_encode(['success' => true, 'html' => $html]);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Calcul impossible : ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -5,9 +5,30 @@ require __DIR__ . '/../../../../includes/auth.php';
|
||||
require __DIR__ . '/../../../../includes/db.php';
|
||||
require_login();
|
||||
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
// =================================================================
|
||||
// LECTURE D'UNE NOTE (AJAX)
|
||||
// =================================================================
|
||||
if ($action === 'get_note') {
|
||||
$noteType = $_POST['note_type'] ?? '';
|
||||
$refId = $_POST['reference_id'] ?? '';
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = ? AND reference_id = ?");
|
||||
$stmt->execute([$noteType, $refId]);
|
||||
|
||||
// fetchColumn() renvoie false si aucune ligne n'est trouvée, on sécurise avec un coalesce (?? '')
|
||||
$content = $stmt->fetchColumn();
|
||||
$content = $content !== false ? $content : '';
|
||||
|
||||
echo json_encode(['success' => true, 'content' => $content]);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 7. SAUVEGARDE D'UNE NOTE GÉNÉRIQUE (pf_notes)
|
||||
// =================================================================
|
||||
@@ -42,7 +63,7 @@ if ($action === 'save_note') {
|
||||
|
||||
// 1. MISE A JOUR TABLEAU SALAIRES (AJAX)
|
||||
if ($action === 'update_salary_config') {
|
||||
header('Content-Type: application/json'); // On précise JSON ici
|
||||
header('Content-Type: application/json');
|
||||
$year = $_POST['year'];
|
||||
$person = $_POST['person'];
|
||||
$field = $_POST['field']; // salary, mensualite, etc.
|
||||
@@ -50,7 +71,10 @@ if ($action === 'update_salary_config') {
|
||||
|
||||
// Liste des champs autorisés pour éviter les injections
|
||||
$allowed = ['salary', 'mensualite', 'frais_func', 'eco_perso', 'eco_family'];
|
||||
if (!in_array($field, $allowed)) { echo json_encode(['error'=>'Champ invalide']); exit; }
|
||||
if (!in_array($field, $allowed)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Champ invalide']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_salary_config (year, person, $field) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE $field = VALUES($field)");
|
||||
$stmt->execute([$year, $person, $value]);
|
||||
@@ -62,28 +86,37 @@ if ($action === 'update_salary_config') {
|
||||
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'
|
||||
$catId = (int)$_POST['cat_id'];
|
||||
$personId = (int)$_POST['person_id'];
|
||||
$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') {
|
||||
try {
|
||||
$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;
|
||||
|
||||
if (!empty($name)) {
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_alloc_categories (name, target, holiday_id) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$name, $target, $holiday_id]);
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_alloc_categories (name, target, transfer_dest, holiday_id) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$name, $target, $transfer_dest, $holiday_id]);
|
||||
}
|
||||
|
||||
// --- NOUVEAU : Réponse AJAX ---
|
||||
if (isset($_POST['ajax'])) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => true]);
|
||||
@@ -91,21 +124,30 @@ if ($action === 'add_category') {
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
// 4. MODIFICATION D'UNE CATEGORIE
|
||||
if ($action === 'update_category') {
|
||||
try {
|
||||
$id = (int)$_POST['cat_id'];
|
||||
$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;
|
||||
|
||||
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]);
|
||||
$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]);
|
||||
@@ -113,6 +155,14 @@ if ($action === 'update_category') {
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
// 5. SUPPRESSION CATEGORIE
|
||||
@@ -136,36 +186,41 @@ 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
|
||||
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'];
|
||||
$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'];
|
||||
|
||||
$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])) {
|
||||
@@ -181,7 +236,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 +254,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 +296,91 @@ if ($action === 'validate_transfers') {
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 7. GESTION DES AVANCES / TRICOUNT (pf_advances)
|
||||
// =================================================================
|
||||
|
||||
if ($action === 'save_advance') {
|
||||
header('Content-Type: application/json');
|
||||
try {
|
||||
$payer = trim($_POST['payer'] ?? '');
|
||||
$advance_date = $_POST['advance_date'] ?? date('Y-m-d');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$amount = abs((float)($_POST['amount'] ?? 0));
|
||||
$from_savings = isset($_POST['from_savings']) ? 1 : 0;
|
||||
|
||||
if (empty($payer) || empty($description) || $amount <= 0) {
|
||||
throw new Exception("Champs obligatoires manquants ou invalides.");
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_advances (advance_date, payer, description, amount, from_savings) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$advance_date, $payer, $description, $amount, $from_savings]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'update_advance') {
|
||||
header('Content-Type: application/json');
|
||||
try {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$payer = trim($_POST['payer'] ?? '');
|
||||
$advance_date = $_POST['advance_date'] ?? date('Y-m-d');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$amount = abs((float)($_POST['amount'] ?? 0));
|
||||
$from_savings = isset($_POST['from_savings']) ? 1 : 0;
|
||||
|
||||
if ($id <= 0 || empty($payer) || empty($description) || $amount <= 0) {
|
||||
throw new Exception("Paramètres invalides fournis.");
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE pf_advances SET advance_date = ?, payer = ?, description = ?, amount = ?, from_savings = ? WHERE id = ?");
|
||||
$stmt->execute([$advance_date, $payer, $description, $amount, $from_savings, $id]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'resolve_advance') {
|
||||
header('Content-Type: application/json');
|
||||
try {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id <= 0) throw new Exception("ID invalide fourni.");
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE pf_advances SET is_resolved = 1 WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete_advance') {
|
||||
header('Content-Type: application/json');
|
||||
try {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id <= 0) throw new Exception("ID invalide.");
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM pf_advances WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
@@ -3,15 +3,23 @@ require __DIR__ . '/../../../../includes/auth.php';
|
||||
require __DIR__ . '/../../../../includes/db.php';
|
||||
require_login();
|
||||
|
||||
// 1. Déclaration propre des variables d'entrée
|
||||
$action = $_POST['action'] ?? '';
|
||||
$isAjax = !empty($_POST['ajax']);
|
||||
|
||||
// Si c'est de l'AJAX, on force les en-têtes HTTP pour garantir du JSON
|
||||
if ($isAjax) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// MISE À JOUR D'UNE CELLULE EN DIRECT (AJAX)
|
||||
// =================================================================
|
||||
if ($action === 'update_single_entry') {
|
||||
header('Content-Type: application/json');
|
||||
$month = $_POST['month_date'];
|
||||
$cat = $_POST['category'];
|
||||
$owner = $_POST['owner'];
|
||||
$amount = (float)$_POST['amount'];
|
||||
$month = $_POST['month_date'] ?? '';
|
||||
$cat = $_POST['category'] ?? '';
|
||||
$owner = $_POST['owner'] ?? '';
|
||||
$amount = (float)($_POST['amount'] ?? 0);
|
||||
|
||||
try {
|
||||
if ($amount == 0 && $cat !== 'TOTAL_BANQUE') {
|
||||
@@ -31,10 +39,10 @@ if ($action === 'update_single_entry') {
|
||||
}
|
||||
|
||||
// --- ACTION : SUPPRESSION D'UNE ENTRÉE UNIQUE ---
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete_entry') {
|
||||
$owner = $_POST['owner'];
|
||||
$date = $_POST['month_date'];
|
||||
$cat = $_POST['category'];
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'delete_entry') {
|
||||
$owner = $_POST['owner'] ?? '';
|
||||
$date = $_POST['month_date'] ?? '';
|
||||
$cat = $_POST['category'] ?? '';
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM pf_savings WHERE owner = ? AND month_date = ? AND category = ?");
|
||||
$stmt->execute([$owner, $date, $cat]);
|
||||
@@ -44,11 +52,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
|
||||
}
|
||||
|
||||
// --- ACTION : DUPLICATION DE MOIS ---
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'duplicate_month') {
|
||||
$owner = $_POST['owner'];
|
||||
$sourceDate = $_POST['source_date'];
|
||||
$targetDate = $_POST['target_date'];
|
||||
$newTotal = floatval($_POST['new_total']);
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'duplicate_month') {
|
||||
$owner = $_POST['owner'] ?? '';
|
||||
$sourceDate = $_POST['source_date'] ?? '';
|
||||
$targetDate = $_POST['target_date'] ?? '';
|
||||
$newTotal = floatval($_POST['new_total'] ?? 0);
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
@@ -78,16 +86,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'dupli
|
||||
echo json_encode(['success' => true]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$pdo->rollBack();
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- ACTION : SUPPRESSION GLOBALE D'UN MOIS ---
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete_month_global') {
|
||||
$owner = $_POST['owner'];
|
||||
$date = $_POST['month_date'];
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'delete_month_global') {
|
||||
$owner = $_POST['owner'] ?? '';
|
||||
$date = $_POST['month_date'] ?? '';
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("DELETE FROM pf_savings WHERE owner = ? AND month_date = ?");
|
||||
@@ -101,18 +109,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
|
||||
}
|
||||
|
||||
// --- ACTION : SAUVEGARDE CLASSIQUE (MODALE) ---
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$owner = $_POST['owner'];
|
||||
|
||||
// CORRECTION ICI : On récupère l'onglet de redirection (Nens, Alex ou Laia)
|
||||
// Cette action s'exécute si c'est un POST général sans action précise (le form HTML) ou l'action explicitement nommée.
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && in_array($action, ['save_modal', ''])) {
|
||||
$owner = $_POST['owner'] ?? '';
|
||||
$redirectTab = $_POST['redirect_tab'] ?? $owner;
|
||||
|
||||
$dateInput = $_POST['month_date'];
|
||||
$dateObj = new DateTime($dateInput);
|
||||
$monthDate = $dateObj->format('Y-m-01');
|
||||
$dateInput = $_POST['month_date'] ?? '';
|
||||
$values = $_POST['values'] ?? []; // Tableau généré par nos champs JS: values[Catégorie]
|
||||
|
||||
try {
|
||||
$dateObj = new DateTime($dateInput);
|
||||
$monthDate = $dateObj->format('Y-m-01');
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// On supprime d'abord les anciennes données du mois pour cet utilisateur
|
||||
@@ -130,11 +138,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
$pdo->commit();
|
||||
|
||||
// Redirection vers le bon onglet
|
||||
// On répond proprement selon le contexte (AJAX ou Fallback classique)
|
||||
if ($isAjax) {
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
header("Location: /budget.php?tab=epargne&owner=" . urlencode($redirectTab));
|
||||
}
|
||||
exit;
|
||||
} catch (Exception $e) {
|
||||
$pdo->rollBack();
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
|
||||
if ($isAjax) {
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
} else {
|
||||
die("Erreur : " . $e->getMessage());
|
||||
}
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Sécurité finale : Si l'action n'est pas reconnue
|
||||
if ($isAjax) {
|
||||
echo json_encode(['success' => false, 'error' => 'Action inconnue']);
|
||||
exit;
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
require __DIR__ . '/../../../../includes/auth.php';
|
||||
require __DIR__ . '/../../../../includes/db.php';
|
||||
require_login();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
$action = $_GET['action'] ?? $_POST['action'] ?? '';
|
||||
|
||||
try {
|
||||
if ($action === 'get_all') {
|
||||
// 1. Comptes bancaires avec le nom du propriétaire lié (pf_people)
|
||||
$accounts = $pdo->query("
|
||||
SELECT a.*, p.name as owner_name
|
||||
FROM pf_bank_accounts a
|
||||
LEFT JOIN pf_people p ON a.owner_person_id = p.id
|
||||
ORDER BY a.is_default DESC, a.name ASC
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 2. Catégories
|
||||
$categories = $pdo->query("SELECT * FROM pf_budget_categories ORDER BY type ASC, label ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 3. Règles d'import
|
||||
$rules = $pdo->query("
|
||||
SELECT r.*, c.label as cat_label
|
||||
FROM pf_import_rules r
|
||||
LEFT JOIN pf_budget_categories c ON r.category COLLATE utf8mb4_unicode_ci = c.code COLLATE utf8mb4_unicode_ci
|
||||
ORDER BY r.keyword ASC
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 4. Salaires (Année en cours)
|
||||
$currentYear = (int)date('Y');
|
||||
$salaries = $pdo->query("
|
||||
SELECT p.name as person,
|
||||
COALESCE(s.salary, 0) as salary,
|
||||
COALESCE(s.mensualite, 0) as mensualite,
|
||||
s.id as id
|
||||
FROM pf_people p
|
||||
LEFT JOIN pf_salary_config s ON p.name = s.person AND s.year = $currentYear
|
||||
WHERE p.role NOT IN ('enfant', 'nounou') AND p.is_active = 1
|
||||
ORDER BY p.name ASC
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 5. Paramètres Foyer (Devise + Mapping CSV)
|
||||
$foyerData = $pdo->query("SELECT currency, csv_mapping FROM pf_foyer_settings LIMIT 1")->fetch(PDO::FETCH_ASSOC);
|
||||
$currencySetting = $foyerData['currency'] ?? '€';
|
||||
$csvMapping = !empty($foyerData['csv_mapping']) ? json_decode($foyerData['csv_mapping'], true) : null;
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'accounts' => $accounts,
|
||||
'categories' => $categories,
|
||||
'rules' => $rules,
|
||||
'salaries' => $salaries,
|
||||
'year' => $currentYear,
|
||||
'currency' => $currencySetting,
|
||||
'csv_mapping' => $csvMapping
|
||||
]
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- GESTION DES COMPTES BANCAIRES ---
|
||||
if ($action === 'add_account') {
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$type = $_POST['type'] ?? 'checking';
|
||||
if (empty($name)) throw new Exception("Le nom du compte est obligatoire.");
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_bank_accounts (name, account_type, is_default) VALUES (?, ?, 0)");
|
||||
$stmt->execute([$name, $type]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete_account') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$count = $pdo->query("SELECT COUNT(*) FROM pf_bank_accounts")->fetchColumn();
|
||||
if ($count <= 1) throw new Exception("Impossible de supprimer le dernier compte.");
|
||||
$stmt = $pdo->prepare("DELETE FROM pf_bank_accounts WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- GESTION DES CATÉGORIES ---
|
||||
if ($action === 'add_category') {
|
||||
$code = strtoupper(trim($_POST['code'] ?? ''));
|
||||
$label = trim($_POST['label'] ?? '');
|
||||
$type = $_POST['type'] ?? 'Expense';
|
||||
$color = $_POST['color'] ?? '#cccccc';
|
||||
$icon = trim($_POST['icon'] ?? '📌');
|
||||
if (empty($code) || empty($label)) throw new Exception("Le code et le libellé sont obligatoires.");
|
||||
$check = $pdo->prepare("SELECT COUNT(*) FROM pf_budget_categories WHERE code = ?");
|
||||
$check->execute([$code]);
|
||||
if ($check->fetchColumn() > 0) throw new Exception("Ce code de catégorie existe déjà.");
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_budget_categories (code, label, type, color, icon) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$code, $label, $type, $color, $icon]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete_category') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$stmt = $pdo->prepare("DELETE FROM pf_budget_categories WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- GESTION DES RÈGLES D'IMPORT ---
|
||||
if ($action === 'add_rule') {
|
||||
$keyword = strtoupper(trim($_POST['keyword'] ?? ''));
|
||||
$category = trim($_POST['category'] ?? '');
|
||||
if (empty($keyword) || empty($category)) throw new Exception("Le mot-clé et la catégorie sont obligatoires.");
|
||||
$check = $pdo->prepare("SELECT COUNT(*) FROM pf_import_rules WHERE keyword = ?");
|
||||
$check->execute([$keyword]);
|
||||
if ($check->fetchColumn() > 0) throw new Exception("Une règle pour ce mot-clé existe déjà.");
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category) VALUES (?, ?)");
|
||||
$stmt->execute([$keyword, $category]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete_rule') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$stmt = $pdo->prepare("DELETE FROM pf_import_rules WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- GESTION DES SALAIRES ---
|
||||
if ($action === 'save_salary') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$salary = (float)($_POST['salary'] ?? 0);
|
||||
$mensualite = (float)($_POST['mensualite'] ?? 0);
|
||||
if ($id <= 0) throw new Exception("ID de configuration de salaire invalide.");
|
||||
$stmt = $pdo->prepare("UPDATE pf_salary_config SET salary = ?, mensualite = ? WHERE id = ?");
|
||||
$stmt->execute([$salary, $mensualite, $id]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- GESTION DE LA DEVISE GLOBALE ---
|
||||
if ($action === 'save_currency') {
|
||||
$currency = trim($_POST['currency'] ?? '€');
|
||||
$stmt = $pdo->prepare("UPDATE pf_foyer_settings SET currency = ?");
|
||||
$stmt->execute([$currency]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- GESTION DU FORMAT CSV ---
|
||||
if ($action === 'save_csv_mapping') {
|
||||
$mapping = [
|
||||
'delimiter' => $_POST['csv_delimiter'] ?? ';',
|
||||
'date_format' => $_POST['csv_date_format'] ?? 'd/m/Y',
|
||||
'col_date' => (int)($_POST['csv_col_date'] ?? 0),
|
||||
'col_label' => (int)($_POST['csv_col_label'] ?? 1),
|
||||
'amount_type' => $_POST['csv_amount_type'] ?? 'single',
|
||||
'col_debit' => (int)($_POST['csv_col_debit'] ?? 8),
|
||||
'col_credit' => (int)($_POST['csv_col_credit'] ?? 9),
|
||||
'col_ref' => (int)($_POST['csv_col_ref'] ?? 3)
|
||||
];
|
||||
$jsonContent = json_encode($mapping);
|
||||
$stmt = $pdo->prepare("UPDATE pf_foyer_settings SET csv_mapping = ?");
|
||||
$stmt->execute([$jsonContent]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// SAUVEGARDE DU NOUVEL ORDRE DES CATÉGORIES
|
||||
if (isset($_POST['action']) && $_POST['action'] === 'save_categories_order') {
|
||||
$ids = json_decode($_POST['order'], true);
|
||||
if (is_array($ids)) {
|
||||
$stmt = $pdo->prepare("UPDATE pf_budget_categories SET sort_order = ? WHERE id = ?");
|
||||
foreach ($ids as $index => $id) {
|
||||
$stmt->execute([$index, (int)$id]);
|
||||
}
|
||||
}
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
throw new Exception("Action API inconnue.");
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,31 @@
|
||||
<?php
|
||||
// modules/budget/views/epargne.php
|
||||
|
||||
$requestedOwner = $_GET['owner'] ?? 'Nens';
|
||||
$ownersToDisplay = ($requestedOwner === 'Nens') ? ['Pol', 'Pep'] : [$requestedOwner];
|
||||
// --- 🧠 CONFIGURATION AGNOSTIQUE DES MEMBRES (MULTI-TENANT) ---
|
||||
$stmtPeople = $pdo->query("SELECT name, role FROM pf_people ORDER BY id ASC");
|
||||
$familyParents = [];
|
||||
$familyKids = [];
|
||||
|
||||
while ($row = $stmtPeople->fetch(PDO::FETCH_ASSOC)) {
|
||||
$role = strtolower(trim($row['role'] ?? ''));
|
||||
|
||||
if ($role === 'parent') {
|
||||
$familyParents[] = $row['name'];
|
||||
} elseif ($role === 'nounou' || $role === 'helper') {
|
||||
// On ignore la nounou dans l'épargne
|
||||
continue;
|
||||
} else {
|
||||
$familyKids[] = $row['name'];
|
||||
}
|
||||
}
|
||||
|
||||
// Sécurité anti-page blanche si la BDD est mal configurée
|
||||
if (empty($familyParents)) $familyParents = ['Parent 1', 'Parent 2'];
|
||||
|
||||
$requestedOwner = $_GET['owner'] ?? (!empty($familyKids) ? 'KIDS' : ($familyParents[0] ?? 'KIDS'));
|
||||
$ownersToDisplay = ($requestedOwner === 'KIDS') ? $familyKids : [$requestedOwner];
|
||||
|
||||
// --- RÉCUPÉRATION CONFIGURATION DES MOIS ---
|
||||
$cycleConfigs = [];
|
||||
$stmtNotes = $pdo->query("SELECT reference_id, content FROM pf_notes WHERE note_type = 'month_config'");
|
||||
while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
|
||||
@@ -14,7 +36,6 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
|
||||
}
|
||||
}
|
||||
|
||||
// Récupération sécurisée du nom des mois (utilise les clés globales existantes)
|
||||
function getMonthName($dateString) {
|
||||
$m = date('m', strtotime($dateString));
|
||||
$y = date('Y', strtotime($dateString));
|
||||
@@ -25,13 +46,22 @@ function getMonthName($dateString) {
|
||||
<div class="budget-view">
|
||||
<div class="view-header">
|
||||
<div class="owner-tabs">
|
||||
<a href="?tab=epargne&owner=Alex" class="owner-tab <?= $requestedOwner === 'Alex' ? 'active' : '' ?>">Alex</a>
|
||||
<a href="?tab=epargne&owner=Laia" class="owner-tab <?= $requestedOwner === 'Laia' ? 'active' : '' ?>">Laia</a>
|
||||
<a href="?tab=epargne&owner=Nens" class="owner-tab <?= $requestedOwner === 'Nens' ? 'active' : '' ?>">Nens 👶</a>
|
||||
|
||||
<?php if (!empty($familyKids)): ?>
|
||||
<a href="?tab=epargne&owner=KIDS" class="owner-tab <?= $requestedOwner === 'KIDS' ? 'active' : '' ?>">
|
||||
<?= tr('budget_tab_kids') ?? 'Enfants 👶' ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($familyParents as $p): ?>
|
||||
<a href="?tab=epargne&owner=<?= urlencode($p) ?>" class="owner-tab <?= $requestedOwner === $p ? 'active' : '' ?>">
|
||||
<?= htmlspecialchars($p) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php foreach ($ownersToDisplay as $currentOwner):
|
||||
<?php foreach ($ownersToDisplay as $index => $currentOwner):
|
||||
$stmt = $pdo->prepare("SELECT month_date, category, amount FROM pf_savings WHERE owner = ? ORDER BY month_date DESC");
|
||||
$stmt->execute([$currentOwner]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
@@ -51,31 +81,29 @@ function getMonthName($dateString) {
|
||||
$months = array_slice($months, 0, 7);
|
||||
sort($allCategories);
|
||||
|
||||
// Définition de la classe couleur selon le propriétaire
|
||||
$ownerTextClass = '';
|
||||
if ($currentOwner === 'Alex') $ownerTextClass = 'txt-alex';
|
||||
elseif ($currentOwner === 'Laia') $ownerTextClass = 'txt-laia';
|
||||
else $ownerTextClass = 'txt-global'; // Pour Pol et Pep
|
||||
$ownerTextClass = 'txt-global';
|
||||
// 🔄 CORRECTION ICI : Création d'un nom "safe" sans espace pour les classes CSS
|
||||
$safeOwnerCls = htmlspecialchars(str_replace(' ', '_', $currentOwner), ENT_QUOTES);
|
||||
?>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; margin-top: <?= ($requestedOwner === 'Nens' && $currentOwner !== 'Pol') ? '40px' : '0' ?>;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; margin-top: <?= ($requestedOwner === 'KIDS' && $index > 0) ? '40px' : '0' ?>;">
|
||||
<div style="flex-grow: 1;">
|
||||
<?php if ($requestedOwner === 'Nens'):
|
||||
$themeClass = 'theme-' . strtolower($currentOwner);
|
||||
<?php if ($requestedOwner === 'KIDS'):
|
||||
$themeClass = 'theme-' . strtolower($safeOwnerCls);
|
||||
?>
|
||||
<h3 class="nens-title <?= $themeClass ?>" style="margin:0; font-size:1.2rem;">
|
||||
<?= $currentOwner ?>
|
||||
<?= htmlspecialchars($currentOwner) ?>
|
||||
</h3>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<?php if (!empty($months)): ?>
|
||||
<button onclick="duplicateLastMonth('<?= $months[0] ?>', '<?= $currentOwner ?>')" class="pf-btn btn-secondary">
|
||||
<button onclick="duplicateLastMonth('<?= $months[0] ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')" class="pf-btn btn-secondary">
|
||||
🔁 <?= tr('bud_sav_add_one_month') ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<button onclick="openCustomSavingsModal('<?= $currentOwner ?>')" class="pf-btn">
|
||||
<button onclick="openCustomSavingsModal('<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')" class="pf-btn">
|
||||
+ <?= tr('bud_sav_add_month') ?>
|
||||
</button>
|
||||
</div>
|
||||
@@ -87,7 +115,7 @@ function getMonthName($dateString) {
|
||||
<p><?= sprintf(tr('bud_sav_no_data'), htmlspecialchars($currentOwner)) ?></p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<table class="pf-table savings-table nens-table theme-<?= strtolower($currentOwner) ?>" style="margin-top:0; box-shadow:none; border-radius:16px;">
|
||||
<table class="pf-table savings-table nens-table theme-<?= strtolower($safeOwnerCls) ?>" style="margin-top:0; box-shadow:none; border-radius:16px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sticky-col" style="background:#f8fafc;"><?= tr('bud_sav_post_month') ?></th>
|
||||
@@ -106,11 +134,11 @@ function getMonthName($dateString) {
|
||||
<div class="month-actions" style="justify-content: center; width: 100%;">
|
||||
<button class="btn-icon-small btn-safe-click" title="<?= tr('bud_sav_edit_modal') ?>"
|
||||
data-json="<?= htmlspecialchars(json_encode($data[$month] ?? []), ENT_QUOTES, 'UTF-8') ?>"
|
||||
onclick='editCustomSavingsMonth("<?= $month ?>", "<?= $currentOwner ?>", JSON.parse(this.getAttribute("data-json")))'>
|
||||
onclick='editCustomSavingsMonth("<?= $month ?>", "<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>", JSON.parse(this.getAttribute("data-json")))'>
|
||||
✏️
|
||||
</button>
|
||||
<button class="btn-icon-small btn-safe-click" title="<?= tr('bud_sav_delete_month') ?>"
|
||||
onclick="deleteEntireMonth('<?= $month ?>', '<?= $currentOwner ?>')"
|
||||
onclick="deleteEntireMonth('<?= $month ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')"
|
||||
style="color: #ef4444; border-color: #fca5a5; background: #fef2f2;">
|
||||
🗑️
|
||||
</button>
|
||||
@@ -129,11 +157,11 @@ function getMonthName($dateString) {
|
||||
<td class="text-center" style="padding:4px;">
|
||||
<div style="display:flex; align-items:center; justify-content:center; gap:2px;">
|
||||
<input type="number" step="0.01"
|
||||
class="prev-input total-input-<?= $currentOwner ?>-<?= $month ?>"
|
||||
class="prev-input total-input-<?= $safeOwnerCls ?>-<?= $month ?>"
|
||||
style="width: 70px; font-weight:bold; color:#2563eb;"
|
||||
value="<?= $val != 0 ? round($val) : '' ?>"
|
||||
placeholder="0"
|
||||
onchange="updateEpargneCell('<?= $month ?>', 'TOTAL_BANQUE', '<?= $currentOwner ?>', this)">
|
||||
onchange="updateEpargneCell('<?= $month ?>', 'TOTAL_BANQUE', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>', this)">
|
||||
<span style="color:#2563eb; font-weight:bold; font-size:0.9rem;">€</span>
|
||||
</div>
|
||||
</td>
|
||||
@@ -149,11 +177,11 @@ function getMonthName($dateString) {
|
||||
<td class="text-center" style="padding:4px;">
|
||||
<div style="display:flex; align-items:center; justify-content:center; gap:2px;">
|
||||
<input type="number" step="0.01"
|
||||
class="prev-input <?= $ownerTextClass ?> cat-input-<?= $currentOwner ?>-<?= $month ?>"
|
||||
class="prev-input <?= $ownerTextClass ?> cat-input-<?= $safeOwnerCls ?>-<?= $month ?>"
|
||||
style="width: 70px;"
|
||||
value="<?= $amount != 0 ? round($amount) : '' ?>"
|
||||
placeholder="-"
|
||||
onchange="updateEpargneCell('<?= $month ?>', '<?= htmlspecialchars($cat, ENT_QUOTES) ?>', '<?= $currentOwner ?>', this)">
|
||||
onchange="updateEpargneCell('<?= $month ?>', '<?= htmlspecialchars($cat, ENT_QUOTES) ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>', this)">
|
||||
</div>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
@@ -168,7 +196,7 @@ function getMonthName($dateString) {
|
||||
foreach ($allCategories as $cat) $sum += ($data[$month][$cat] ?? 0);
|
||||
$extra = $total - $sum;
|
||||
?>
|
||||
<td class="text-center font-bold sum-target" id="extra_<?= $currentOwner ?>_<?= $month ?>" style="color: <?= $extra >= 0 ? '#10b981' : '#ef4444' ?>; padding:12px;">
|
||||
<td class="text-center font-bold sum-target" id="extra_<?= $safeOwnerCls ?>_<?= $month ?>" style="color: <?= $extra >= 0 ? '#10b981' : '#ef4444' ?>; padding:12px;">
|
||||
<?= number_format($extra, 0, ',', ' ') ?> €
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
@@ -260,10 +288,13 @@ window.I18N = {
|
||||
'bud_err_delete': <?= json_encode(tr('bud_err_delete')) ?>
|
||||
};
|
||||
|
||||
// --- 2. GESTION DE L'ÉDITION INVISIBLE EN DIRECT ---
|
||||
// Sécurisation de la devise pour la calculatrice
|
||||
const systemCurrency = (typeof window.CONFIG !== 'undefined' && window.CONFIG.CURRENCY) ? window.CONFIG.CURRENCY : '€';
|
||||
|
||||
// --- 2. GESTION DE L'ÉDITION INVISIBLE EN DIRECT (CORRIGÉE 🚀) ---
|
||||
const cycleConfigs = <?= json_encode($cycleConfigs ?? []) ?>;
|
||||
|
||||
function updateEpargneCell(month, category, owner, inputEl) {
|
||||
async function updateEpargneCell(month, category, owner, inputEl) {
|
||||
const val = parseFloat(inputEl.value) || 0;
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'update_single_entry');
|
||||
@@ -271,22 +302,38 @@ function updateEpargneCell(month, category, owner, inputEl) {
|
||||
formData.append('category', category);
|
||||
formData.append('owner', owner);
|
||||
formData.append('amount', val);
|
||||
formData.append('ajax', '1'); // 🚀 CRITIQUE : Informe l'API
|
||||
|
||||
fetch('modules/budget/includes/api/save-savings.php', {
|
||||
try {
|
||||
// 🚀 CRITIQUE : pachaFetch + Chemin absolu
|
||||
const result = await pachaFetch('/modules/budget/includes/api/save-savings.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).catch(err => alert(window.I18N['bud_err_tech'] || 'Erreur technique'));
|
||||
});
|
||||
|
||||
const totalInput = document.querySelector(`.total-input-${owner}-${month}`);
|
||||
if (!result.success) {
|
||||
console.error("Erreur de sauvegarde :", result.error);
|
||||
alert(window.I18N['bud_err_tech'] + " : " + (result.error || "Erreur serveur"));
|
||||
return; // On stoppe la mise à jour visuelle si la BDD a refusé
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Erreur réseau :", err);
|
||||
alert(window.I18N['bud_err_tech'] || 'Erreur technique');
|
||||
return;
|
||||
}
|
||||
|
||||
// Si on arrive ici, la BDD est bien à jour ! On met à jour le visuel.
|
||||
const safeOwnerClass = owner.replace(/\s+/g, '_');
|
||||
const totalInput = document.querySelector(`.total-input-${CSS.escape(safeOwnerClass)}-${month}`);
|
||||
const totalVal = parseFloat(totalInput ? totalInput.value : 0) || 0;
|
||||
|
||||
let sumCats = 0;
|
||||
document.querySelectorAll(`.cat-input-${owner}-${month}`).forEach(inp => {
|
||||
document.querySelectorAll(`.cat-input-${CSS.escape(safeOwnerClass)}-${month}`).forEach(inp => {
|
||||
sumCats += parseFloat(inp.value) || 0;
|
||||
});
|
||||
|
||||
const extra = totalVal - sumCats;
|
||||
const extraCell = document.getElementById(`extra_${owner}_${month}`);
|
||||
const extraCell = document.getElementById(`extra_${safeOwnerClass}_${month}`);
|
||||
|
||||
if (extraCell) {
|
||||
extraCell.innerText = Math.round(extra).toLocaleString(window.appLang) + ' €';
|
||||
@@ -390,7 +437,7 @@ window.onclick = function(event) {
|
||||
|
||||
const savingsForm = document.getElementById('savingsForm');
|
||||
if (savingsForm) {
|
||||
savingsForm.addEventListener('submit', function(e) {
|
||||
savingsForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const ym = document.getElementById('sav_month').value;
|
||||
@@ -404,23 +451,32 @@ if (savingsForm) {
|
||||
submitBtn.disabled = true;
|
||||
|
||||
const formData = new FormData(this);
|
||||
formData.append('ajax', '1'); // 🚀 CRITIQUE
|
||||
|
||||
const actionUrl = this.getAttribute('action');
|
||||
const finalUrl = actionUrl.startsWith('/') ? actionUrl.substring(1) : actionUrl;
|
||||
try {
|
||||
// 🚀 CRITIQUE : pachaFetch + Chemin absolu forcé
|
||||
const result = await pachaFetch('/modules/budget/includes/api/save-savings.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
fetch(finalUrl, { method: 'POST', body: formData })
|
||||
.then(response => response.text())
|
||||
.then(text => { window.location.reload(); })
|
||||
.catch(error => {
|
||||
console.error("Erreur:", error);
|
||||
if (result.success) {
|
||||
window.location.reload(); // Ne recharge QUE si le serveur a dit OK
|
||||
} else {
|
||||
alert((window.I18N['bud_err_server'] || 'Erreur serveur : ') + (result.error || "Inconnue"));
|
||||
submitBtn.innerText = originalText;
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur catch:", error);
|
||||
alert(window.I18N['bud_err_tech'] || 'Erreur Technique');
|
||||
submitBtn.innerText = originalText;
|
||||
submitBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteEntireMonth(monthDate, owner) {
|
||||
async function deleteEntireMonth(monthDate, owner) {
|
||||
const rawMsg = window.I18N['bud_sav_confirm_delete_month'] || "Supprimer %m pour %o ?";
|
||||
const msg = rawMsg.replace('%m', monthDate).replace('%o', owner);
|
||||
if (!confirm(msg)) return;
|
||||
@@ -429,13 +485,18 @@ function deleteEntireMonth(monthDate, owner) {
|
||||
formData.append("action", "delete_month_global");
|
||||
formData.append("month_date", monthDate);
|
||||
formData.append("owner", owner);
|
||||
formData.append("ajax", "1"); // 🚀 CRITIQUE
|
||||
|
||||
fetch("modules/budget/includes/api/save-savings.php", { method: "POST", body: formData })
|
||||
.then(() => window.location.reload())
|
||||
.catch(err => alert(window.I18N['bud_err_delete'] || 'Erreur lors de la suppression'));
|
||||
try {
|
||||
const result = await pachaFetch('/modules/budget/includes/api/save-savings.php', { method: "POST", body: formData });
|
||||
if (result.success) window.location.reload();
|
||||
else alert(result.error || window.I18N['bud_err_delete']);
|
||||
} catch(err) {
|
||||
alert(window.I18N['bud_err_delete'] || 'Erreur lors de la suppression');
|
||||
}
|
||||
}
|
||||
|
||||
function duplicateLastMonth(lastMonthDate, owner) {
|
||||
async function duplicateLastMonth(lastMonthDate, owner) {
|
||||
let dateObj = new Date(lastMonthDate);
|
||||
dateObj.setMonth(dateObj.getMonth() + 1);
|
||||
let year = dateObj.getFullYear();
|
||||
@@ -447,16 +508,13 @@ function duplicateLastMonth(lastMonthDate, owner) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
};
|
||||
|
||||
const sourceName = formatMonth(lastMonthDate);
|
||||
const targetName = formatMonth(nextMonthStr);
|
||||
|
||||
let defaultTotal = "";
|
||||
if (cycleConfigs[nextMonthStr] && cycleConfigs[nextMonthStr].start_balance !== undefined) {
|
||||
defaultTotal = cycleConfigs[nextMonthStr].start_balance;
|
||||
}
|
||||
|
||||
const rawMsg = window.I18N['bud_sav_prompt_duplicate'] || "Dupliquer %s vers %t1 ?";
|
||||
const message = rawMsg.replace('%s', sourceName).replace('%t1', targetName).replace('%t2', targetName);
|
||||
const rawMsg = window.I18N['bud_sav_prompt_duplicate'] || "Dupliquer vers le mois suivant ?";
|
||||
const message = rawMsg.replace('%s', formatMonth(lastMonthDate)).replace(/%t[12]/g, formatMonth(nextMonthStr));
|
||||
|
||||
let newTotal = prompt(message, defaultTotal);
|
||||
|
||||
@@ -467,22 +525,19 @@ function duplicateLastMonth(lastMonthDate, owner) {
|
||||
formData.append("target_date", nextMonthStr);
|
||||
formData.append("new_total", newTotal);
|
||||
formData.append("owner", owner);
|
||||
formData.append("ajax", "1"); // 🚀 CRITIQUE
|
||||
|
||||
fetch("modules/budget/includes/api/save-savings.php", { method: "POST", body: formData })
|
||||
.then(async r => {
|
||||
const text = await r.text();
|
||||
try {
|
||||
const d = JSON.parse(text);
|
||||
if (d.success) window.location.reload();
|
||||
else alert((window.I18N['bud_err_server'] || 'Erreur serveur : ') + (d.error || "Inconnue"));
|
||||
} catch(e) {
|
||||
const result = await pachaFetch('/modules/budget/includes/api/save-savings.php', { method: "POST", body: formData });
|
||||
if (result.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert((window.I18N['bud_err_server'] || 'Erreur serveur : ') + (result.error || "Inconnue"));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
alert(window.I18N['bud_err_network_dup'] || 'Erreur réseau.');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,7 +583,7 @@ function updateSumResult() {
|
||||
total += val;
|
||||
});
|
||||
|
||||
document.getElementById('sumResultValue').innerText = Math.round(total).toLocaleString(window.appLang) + ' ' + window.CONFIG.CURRENCY;
|
||||
document.getElementById('sumResultValue').innerText = Math.round(total).toLocaleString(window.appLang) + ' ' + systemCurrency;
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
// modules/budget/views/provisions.php
|
||||
|
||||
// --- RÉCUPÉRATION DES DÉPENSES PRÉVUES (MULTI-TENANT PDO) ---
|
||||
$stmt = $pdo->query("SELECT id, title, amount, expected_date FROM pf_expected_expenses WHERE is_paid = 0 ORDER BY expected_date ASC");
|
||||
$provisions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// --- RÉCUPÉRATION DES APPORTS ET DETTES (CLEARING) ---
|
||||
$currentYear = date('Y');
|
||||
|
||||
// 1. Apports de base (Eco Family) des parents configurés pour l'année
|
||||
$stmtConfig = $pdo->prepare("SELECT person, eco_family FROM pf_salary_config WHERE year = ?");
|
||||
$stmtConfig->execute([$currentYear]);
|
||||
$configs = $stmtConfig->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
// 2. Dettes non résolues par personne (issues des Avances & Tricount)
|
||||
$stmtDebts = $pdo->query("SELECT payer, SUM(amount) as total_debt FROM pf_advances WHERE is_resolved = 0 GROUP BY payer");
|
||||
$debts = $stmtDebts->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
function formatDate($dateString) {
|
||||
if (!$dateString) return '';
|
||||
return date('d/m/Y', strtotime($dateString));
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="budget-view">
|
||||
<div class="view-header">
|
||||
<h2><?= tr('budget_provisions_title') ?></h2>
|
||||
|
||||
<button class="pf-btn pf-btn-primary" onclick="openOptimizeModal()">
|
||||
✨ <?= tr('btn_optimize_cashflow') ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="pf-card">
|
||||
<h3 class="pf-card-h2"><?= tr('add_new_provision') ?></h3>
|
||||
|
||||
<form id="form-add-provision" data-action="/modules/budget/includes/api/add-provision.php">
|
||||
<div class="provisions-form-grid">
|
||||
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label" for="prov-title"><?= tr('provision_label') ?></label>
|
||||
<input type="text" id="prov-title" name="title" class="pf-input" placeholder="<?= tr('provision_placeholder_wood') ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label" for="prov-amount"><?= tr('amount') ?> (€)</label>
|
||||
<input type="number" id="prov-amount" name="amount" class="pf-input no-spinners input-amount-highlight" step="0.01" min="0.01" required>
|
||||
</div>
|
||||
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label" for="prov-date"><?= tr('expected_date') ?></label>
|
||||
<input type="date" id="prov-date" name="expected_date" class="pf-input" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="pf-btn pf-btn-secondary">
|
||||
<?= tr('btn_add') ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="budget-table-card table-responsive">
|
||||
<?php if (empty($provisions)): ?>
|
||||
<div class="optimization-hint" style="padding: 30px;">
|
||||
<p><?= tr('no_provisions') ?></p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<table class="pf-table savings-table provisions-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= tr('provision_label') ?></th>
|
||||
<th style="width: 150px;"><?= tr('amount') ?></th>
|
||||
<th style="width: 150px;"><?= tr('expected_date') ?></th>
|
||||
<th style="width: 100px;"><?= tr('actions') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($provisions as $p): ?>
|
||||
<tr>
|
||||
<td class="prov-title-cell">
|
||||
<?= htmlspecialchars($p['title']) ?>
|
||||
</td>
|
||||
<td class="prov-amount-cell">
|
||||
<?= number_format($p['amount'], 2, ',', ' ') ?> €
|
||||
</td>
|
||||
<td class="prov-date-cell">
|
||||
<?= formatDate($p['expected_date']) ?>
|
||||
</td>
|
||||
<td class="text-right" style="padding-right: 15px;">
|
||||
<button class="btn-icon-action delete btn-safe-click" title="<?= tr('btn_delete') ?>" onclick="deleteProvision(<?= $p['id'] ?>)">
|
||||
🗑️
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="optimizeModal" class="pf-modal">
|
||||
<div class="pf-modal-content provisions-modal-content">
|
||||
|
||||
<div class="pf-modal-header">
|
||||
<h3 class="pf-modal-title">🧠 <?= tr('optimization_assistant_title') ?></h3>
|
||||
<button type="button" class="pf-modal-close" onclick="closeOptimizeModal()">×</button>
|
||||
</div>
|
||||
|
||||
<form id="form-optimize" data-action="/modules/budget/includes/api/optimize-cashflow.php">
|
||||
<div id="dynamic-savings-inputs">
|
||||
<?php if (empty($configs)): ?>
|
||||
<p class="text-danger font-bold pf-muted-tiny"><?= tr('budget_opti_no_config') ?></p>
|
||||
<input type="number" step="0.01" min="0" name="savings[Global]" class="pf-input no-spinners" required>
|
||||
<?php else: ?>
|
||||
<?php foreach ($configs as $person => $ecoBase):
|
||||
$debt = $debts[$person] ?? 0;
|
||||
?>
|
||||
<div class="pf-form-group provision-person-card">
|
||||
<label class="pf-label provision-person-header">
|
||||
<strong class="text-main"><?= htmlspecialchars($person) ?></strong>
|
||||
<span class="pf-muted-note">
|
||||
<?php if ($debt > 0): ?>
|
||||
<span class="text-danger font-bold">(Dette en attente : <?= (float)$debt ?> €)</span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</label>
|
||||
<div class="provision-input-group">
|
||||
<input type="number" step="0.01" min="0" name="savings[<?= htmlspecialchars($person) ?>]" class="pf-input no-spinners input-amount-success" value="<?= (float)$ecoBase ?>" required>
|
||||
<span class="currency-symbol">€</span>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="pf-btn pf-btn-primary btn-block">
|
||||
<?= tr('calculate') ?>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<hr class="pf-divider">
|
||||
|
||||
<div id="optimization-results">
|
||||
<p class="optimization-hint">
|
||||
Vérifiez vos apports théoriques ci-dessus pour calculer la stratégie de répartition (Clearing automatique si aucune dépense).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
||||
window.I18N = {
|
||||
...(window.I18N || {}),
|
||||
'btn_delete': <?= json_encode(tr('btn_delete')) ?>,
|
||||
'error_generic': <?= json_encode(tr('error_generic')) ?>,
|
||||
'bud_err_tech': <?= json_encode(tr('bud_err_tech')) ?>,
|
||||
'bud_err_server': <?= json_encode(tr('bud_err_server')) ?>
|
||||
};
|
||||
|
||||
// --- 2. GESTION DU FORMULAIRE D'AJOUT ---
|
||||
const formAdd = document.getElementById('form-add-provision');
|
||||
if (formAdd) {
|
||||
formAdd.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const submitBtn = this.querySelector('button[type="submit"]');
|
||||
const originalText = submitBtn.innerText;
|
||||
submitBtn.innerText = '⏳';
|
||||
submitBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const result = await pachaFetch(this.getAttribute('data-action'), {
|
||||
method: 'POST',
|
||||
body: new FormData(this)
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(result.message || window.I18N['error_generic']);
|
||||
submitBtn.innerText = originalText;
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
} catch (error) {
|
||||
alert(window.I18N['bud_err_tech']);
|
||||
submitBtn.innerText = originalText;
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- 3. GESTION DE LA SUPPRESSION ---
|
||||
async function deleteProvision(id) {
|
||||
if (!confirm(window.I18N['btn_delete'] + ' ?')) return;
|
||||
try {
|
||||
const result = await pachaFetch('/modules/budget/includes/api/delete-provision.php', {
|
||||
method: "POST",
|
||||
body: new URLSearchParams({ id: id })
|
||||
});
|
||||
if (result.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(result.message);
|
||||
}
|
||||
} catch(err) {
|
||||
alert(window.I18N['bud_err_tech']);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 4. CONTROL DE LA MODALE D'OPTIMISATION ---
|
||||
function openOptimizeModal() {
|
||||
document.getElementById('optimization-results').innerHTML = `
|
||||
<p class="optimization-hint">
|
||||
Vérifiez vos apports théoriques ci-dessus pour calculer la stratégie de répartition (Clearing automatique si aucune dépense).
|
||||
</p>`;
|
||||
document.getElementById('optimizeModal').classList.add('open');
|
||||
document.body.classList.add('no-scroll');
|
||||
}
|
||||
|
||||
function closeOptimizeModal() {
|
||||
document.getElementById('optimizeModal').classList.remove('open');
|
||||
document.body.classList.remove('no-scroll');
|
||||
}
|
||||
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('optimizeModal');
|
||||
if (event.target == modal) {
|
||||
closeOptimizeModal();
|
||||
}
|
||||
}
|
||||
|
||||
// --- 5. EXECUTION ET SUBMIT DE L'ALGORITHME (formOptimize) ---
|
||||
const formOptimize = document.getElementById('form-optimize');
|
||||
if (formOptimize) {
|
||||
formOptimize.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const resultsContainer = document.getElementById('optimization-results');
|
||||
const submitBtn = this.querySelector('button[type="submit"]');
|
||||
|
||||
submitBtn.disabled = true;
|
||||
resultsContainer.innerHTML = '<div class="optimization-hint">⏳ Analyse des quinzaines bancaires...</div>';
|
||||
|
||||
try {
|
||||
const result = await pachaFetch(this.getAttribute('data-action'), {
|
||||
method: 'POST',
|
||||
body: new FormData(this)
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
resultsContainer.innerHTML = result.html;
|
||||
} else {
|
||||
resultsContainer.innerHTML = `<p class="text-danger font-bold text-center">${result.message}</p>`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur d'optimisation :", error);
|
||||
resultsContainer.innerHTML = `<p class="text-danger font-bold text-center">${window.I18N['bud_err_tech']}</p>`;
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
+204
-242
@@ -1,46 +1,60 @@
|
||||
<?php
|
||||
// modules/budget/views/recap.php
|
||||
|
||||
// 1. Récupération des Items du Budget
|
||||
$stmt = $pdo->query("SELECT * FROM pf_budget_items ORDER BY category DESC, sort_order ASC, name ASC");
|
||||
$items = $stmt->fetchAll();
|
||||
// 1. Récupération des Catégories dynamiques
|
||||
$stmtCats = $pdo->query("SELECT code, label, icon, type FROM pf_budget_categories ORDER BY type DESC, label ASC");
|
||||
$dbCategories = $stmtCats->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 2. Déterminer quel est le mois de gestion "ouvert" par défaut
|
||||
// 2. Récupération des Salaires/Mensualités configurés (Revenus automatiques)
|
||||
$currentYear = isset($_GET['y']) ? (int)$_GET['y'] : date('Y');
|
||||
$stmtSalaries = $pdo->prepare("SELECT id, person, mensualite FROM pf_salary_config WHERE year = ?");
|
||||
$stmtSalaries->execute([$currentYear]);
|
||||
$salaries = $stmtSalaries->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 3. Récupération des Items du Budget (Charges Fixes et Estimations)
|
||||
$stmt = $pdo->query("SELECT * FROM pf_budget_items WHERE category != 'SAVINGS' ORDER BY is_estimate ASC, sort_order ASC, name ASC");
|
||||
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 4. Gestion du mois actif
|
||||
$stmtActive = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'active_gestion_month' LIMIT 1");
|
||||
$defaultActiveMonth = $stmtActive->fetchColumn();
|
||||
if (!$defaultActiveMonth) {
|
||||
$defaultActiveMonth = date('Y-m-01');
|
||||
}
|
||||
|
||||
// 3. Assigner le mois et l'année en fonction du mois ouvert
|
||||
$defaultActiveMonth = $stmtActive->fetchColumn() ?: date('Y-m-01');
|
||||
$currentMonth = isset($_GET['m']) ? str_pad((int)$_GET['m'], 2, '0', STR_PAD_LEFT) : date('m', strtotime($defaultActiveMonth));
|
||||
$currentYear = isset($_GET['y']) ? (int)$_GET['y'] : date('Y', strtotime($defaultActiveMonth));
|
||||
$viewMonthDate = "$currentYear-$currentMonth-01";
|
||||
|
||||
$sqlReal = "SELECT budget_item_id, SUM(amount) as total_real
|
||||
// 5. Récupération optimisée et unifiée du Réel
|
||||
// On récupère tout en une seule requête pour éviter les décalages entre ID et Catégorie
|
||||
$stmtReal = $pdo->prepare("
|
||||
SELECT budget_item_id, category, SUM(amount) as total_real
|
||||
FROM pf_expenses
|
||||
WHERE gestion_month = ? AND budget_item_id IS NOT NULL
|
||||
GROUP BY budget_item_id";
|
||||
$stmtReal = $pdo->prepare($sqlReal);
|
||||
WHERE gestion_month = ?
|
||||
GROUP BY budget_item_id, category
|
||||
");
|
||||
$stmtReal->execute([$viewMonthDate]);
|
||||
$realTotals = $stmtReal->fetchAll(PDO::FETCH_KEY_PAIR); // Retourne un tableau [id => total]
|
||||
$allExpenses = $stmtReal->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$sqlCatReal = "SELECT category, SUM(amount) as total_real
|
||||
FROM pf_expenses
|
||||
WHERE gestion_month = ? AND budget_item_id IS NULL
|
||||
GROUP BY category";
|
||||
$stmtCatReal = $pdo->prepare($sqlCatReal);
|
||||
$stmtCatReal->execute([$viewMonthDate]);
|
||||
$catTotals = $stmtCatReal->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
// On initialise les tableaux de correspondance
|
||||
$realTotalsById = []; // Pour les lignes liées par ID
|
||||
$realTotalsByCat = []; // Pour les lignes orphelines (NULL) liées par Catégorie
|
||||
|
||||
$stmtLabels = $pdo->prepare("SELECT label, amount FROM pf_expenses WHERE gestion_month = ? AND budget_item_id IS NULL");
|
||||
foreach ($allExpenses as $row) {
|
||||
if (!empty($row['budget_item_id'])) {
|
||||
$realTotalsById[$row['budget_item_id']] = (float)$row['total_real'];
|
||||
} else {
|
||||
$realTotalsByCat[$row['category']] = (float)$row['total_real'];
|
||||
}
|
||||
}
|
||||
|
||||
$stmtSalariesReal = $pdo->prepare("SELECT salary_id, SUM(amount) as total_salary FROM pf_expenses WHERE gestion_month = ? AND salary_id IS NOT NULL GROUP BY salary_id");
|
||||
$stmtSalariesReal->execute([$viewMonthDate]);
|
||||
$realTotalsBySalary = $stmtSalariesReal->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
// Récupération des détails pour le mapping par mots-clés (si besoin de précision)
|
||||
$stmtLabels = $pdo->prepare("SELECT label, amount, category FROM pf_expenses WHERE gestion_month = ? AND budget_item_id IS NULL");
|
||||
$stmtLabels->execute([$viewMonthDate]);
|
||||
$unlinkedExpenses = $stmtLabels->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$moisFr = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
|
||||
$monthTranslationKey = 'month_' . str_pad((int)$currentMonth, 2, '0', STR_PAD_LEFT);
|
||||
$currentMonthName = tr($monthTranslationKey) . ' ' . $currentYear;
|
||||
|
||||
// Gestion des libellés et totaux
|
||||
$currentMonthName = tr('month_' . str_pad((int)$currentMonth, 2, '0', STR_PAD_LEFT)) . ' ' . $currentYear;
|
||||
$totalDepenses = 0;
|
||||
$totalRevenus = 0;
|
||||
?>
|
||||
@@ -64,35 +78,69 @@ $totalRevenus = 0;
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($items as $item):
|
||||
// --- 1. CALCUL DES TOTAUX PRÉVUS ---
|
||||
<?php foreach ($salaries as $salary):
|
||||
$mensualite = (float)$salary['mensualite'];
|
||||
$totalRevenus += $mensualite;
|
||||
|
||||
// Calcul du réel perçu
|
||||
$realPerçu = isset($realTotalsBySalary[$salary['id']]) ? (float)$realTotalsBySalary[$salary['id']] : 0;
|
||||
$gap = $realPerçu - $mensualite;
|
||||
$isAutoChecked = ($realPerçu >= ($mensualite - 0.10));
|
||||
?>
|
||||
<tr class="row-income" style="border-bottom:1px solid var(--border-light); background:#f0fdf4;">
|
||||
<td style="padding:15px;">
|
||||
<strong>Apport <?= htmlspecialchars($salary['person']) ?></strong>
|
||||
<span title="Le montant cible est défini dans les paramètres" style="font-size:0.8rem; cursor:help;">⚙️</span>
|
||||
</td>
|
||||
<td class="cell-amount" style="font-weight:600; padding:15px; color:#10b981;">
|
||||
+ <?= number_format($mensualite, 2, ',', ' ') ?> €
|
||||
|
||||
<?php if ($realPerçu > 0): ?>
|
||||
<?php if ($gap > 0.05): ?>
|
||||
<div style="font-size:0.75rem; color:#10b981; font-weight:bold;">Bonus : +<?= number_format($gap, 2, ',', ' ') ?> €</div>
|
||||
<?php elseif ($gap < -0.05): ?>
|
||||
<div style="font-size:0.75rem; color:#f59e0b;">Manque : <?= number_format(abs($gap), 2, ',', ' ') ?> €</div>
|
||||
<?php else: ?>
|
||||
<div style="font-size:0.75rem; color:#10b981;">Atteint ✓</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td style="padding:15px;">
|
||||
<span class="badge-type" style="background:#dcfce7; color:#16a34a; padding:4px 8px; border-radius:12px; font-size:0.8rem; font-weight:600;">Revenu Mensuel</span>
|
||||
</td>
|
||||
<td style="padding:15px; color:#64748b; font-weight:bold;">-</td>
|
||||
<td style="padding:15px;">
|
||||
<?php if ($isAutoChecked): ?>
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#dcfce7; color:#16a34a; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #bbf7d0;"><span>✓</span> Reçu</div>
|
||||
<?php elseif ($realPerçu > 0): ?>
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#fffbeb; color:#d97706; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #fde68a;"><span>⏳</span> Partiel</div>
|
||||
<?php else: ?>
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#f8fafc; color:#94a3b8; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #e2e8f0;"><span>○</span> En attente</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td style="padding:15px; text-align:right;">
|
||||
<small style="color:#94a3b8;">Via Paramètres</small>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php
|
||||
foreach ($items as $item):
|
||||
// 1. Calculs de base
|
||||
$targetAbs = abs((float)$item['amount']);
|
||||
$amountToAdd = ($item['type'] === 'Annuel') ? $targetAbs / 12 : $targetAbs;
|
||||
$totalDepenses += $amountToAdd;
|
||||
|
||||
if ($item['category'] === 'income') $totalRevenus += $amountToAdd;
|
||||
else $totalDepenses += $amountToAdd;
|
||||
|
||||
// --- 2. CALCUL DU RÉEL (Logique optimisée) ---
|
||||
// 2. RÉINITIALISATION STRICTE
|
||||
$realSum = 0;
|
||||
$hasMatchingExpense = false;
|
||||
|
||||
// A. Correspondance directe par ID
|
||||
if (isset($realTotals[$item['id']])) {
|
||||
$realSum = $realTotals[$item['id']];
|
||||
// A. Correspondance par ID direct (Le lien dur)
|
||||
if (isset($realTotalsById[$item['id']])) {
|
||||
$realSum = (float)$realTotalsById[$item['id']];
|
||||
$hasMatchingExpense = true;
|
||||
}
|
||||
// B. Correspondance par catégorie système (École, Essence, FMCG)
|
||||
else {
|
||||
$catKey = null;
|
||||
if (trim($item['name']) === 'Estimacio escola') $catKey = 'School';
|
||||
elseif (trim($item['name']) === 'Estimation gasolina') $catKey = 'Essence';
|
||||
elseif (trim($item['name']) === 'Estimacio F&B & beauty') $catKey = 'FMCG';
|
||||
|
||||
if ($catKey && isset($catTotals[$catKey])) {
|
||||
$realSum = $catTotals[$catKey];
|
||||
$hasMatchingExpense = true;
|
||||
}
|
||||
// C. Correspondance par mots-clés (seulement sur les dépenses non liées)
|
||||
// B. Correspondance par mots-clés bancaires (Le lien intelligent pour les orphelines)
|
||||
elseif (!empty($item['mapping_keywords'])) {
|
||||
$keywords = array_map('trim', explode(',', $item['mapping_keywords']));
|
||||
foreach ($unlinkedExpenses as $uexp) {
|
||||
@@ -100,88 +148,78 @@ $totalRevenus = 0;
|
||||
if (!empty($kw) && stripos($uexp['label'], $kw) !== false) {
|
||||
$realSum += (float)$uexp['amount'];
|
||||
$hasMatchingExpense = true;
|
||||
break;
|
||||
}
|
||||
break; // On a trouvé un mot clé sur cette dépense, on passe à l'orpheline suivante
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// C. Absorption des orphelines par catégories (Le lien Macro/Micro)
|
||||
if (!empty($item['category'])) {
|
||||
$catCodes = array_map('trim', explode(',', $item['category']));
|
||||
foreach ($catCodes as $cCode) {
|
||||
// Si la catégorie existe dans les orphelines et n'a pas encore été consommée
|
||||
if (!empty($cCode) && isset($realTotalsByCat[$cCode]) && $realTotalsByCat[$cCode] != 0) {
|
||||
$realSum += $realTotalsByCat[$cCode];
|
||||
$hasMatchingExpense = true;
|
||||
// On la vide pour ne pas la compter 2 fois si une autre ligne utilise la même catégorie
|
||||
$realTotalsByCat[$cCode] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Finalisation des variables d'affichage
|
||||
$realAbs = abs($realSum);
|
||||
$isAutoChecked = ($hasMatchingExpense && ($realAbs >= ($targetAbs - 0.10)));
|
||||
|
||||
$rowClass = ($item['category'] === 'income') ? 'row-income' : 'row-expense';
|
||||
if ($item['is_estimate']) $rowClass .= ' row-estimate';
|
||||
$rowClass = 'row-expense' . ($item['is_estimate'] ? ' row-estimate' : '');
|
||||
?>
|
||||
<tr class="<?= $rowClass ?>" style="border-bottom:1px solid var(--border-light);">
|
||||
<td style="padding:15px;">
|
||||
<strong><?= htmlspecialchars($item['name']) ?></strong>
|
||||
<?= $item['is_estimate'] ? ' <small style="color:#64748b;">('.tr('bud_est_short').')</small>' : '' ?>
|
||||
|
||||
<?= $item['is_estimate'] ? ' <small style="color:#64748b;">(Variable)</small>' : '' ?>
|
||||
<?php if(!empty($item['mapping_keywords'])): ?>
|
||||
<span title="<?= htmlspecialchars($item['mapping_keywords']) ?>" style="font-size:0.7rem; cursor:help;">🔗</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if(!empty($item['reg_month'])): ?>
|
||||
<div style="font-size:0.75rem; color:#94a3b8; font-style:italic; margin-top:2px;">
|
||||
📅 <?= sprintf(tr('bud_reg_planned_in'), tr('month_'.str_pad(array_search($item['reg_month'], $moisFr), 2, '0', STR_PAD_LEFT))) ?>
|
||||
</div>
|
||||
<span title="Reconnaissance bancaire : <?= htmlspecialchars($item['mapping_keywords']) ?>" style="font-size:0.7rem; cursor:help;">🔗</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<td class="cell-amount" style="font-weight:600; padding:15px; color:<?= $item['category']==='income'?'#10b981':'#1e293b' ?>;">
|
||||
<?= number_format($targetAbs, 2, ',', ' ') ?> €
|
||||
<td class="cell-amount" style="font-weight:600; padding:15px; color:#1e293b;">
|
||||
- <?= number_format($targetAbs, 2, ',', ' ') ?> €
|
||||
|
||||
<?php if ($hasMatchingExpense): ?>
|
||||
<?php
|
||||
// Le "Gap" est la différence visuelle.
|
||||
$gap = $realAbs - $targetAbs;
|
||||
|
||||
<?php $gap = $realAbs - $targetAbs;
|
||||
if ($gap > 0.05): ?>
|
||||
<div style="font-size:0.75rem; color:#ef4444; font-weight:bold;">
|
||||
<?= $item['category'] === 'income' ? tr('bud_bonus') : tr('bud_overrun') ?> : +<?= number_format($gap, 2, ',', ' ') ?> €
|
||||
</div>
|
||||
<div style="font-size:0.75rem; color:#ef4444; font-weight:bold;">Dépassé : +<?= number_format($gap, 2, ',', ' ') ?> €</div>
|
||||
<?php elseif ($gap < -0.05): ?>
|
||||
<div style="font-size:0.75rem; color:#f59e0b; font-weight:normal;">
|
||||
<?= tr('bud_remaining') ?> : <?= number_format(abs($gap), 2, ',', ' ') ?> €
|
||||
</div>
|
||||
<div style="font-size:0.75rem; color:#f59e0b;">Reste : <?= number_format(abs($gap), 2, ',', ' ') ?> €</div>
|
||||
<?php else: ?>
|
||||
<div style="font-size:0.75rem; color:#10b981; font-weight:normal;">
|
||||
<?= tr('bud_exact_amount') ?> ✓
|
||||
</div>
|
||||
<div style="font-size:0.75rem; color:#10b981;">Atteint ✓</div>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($item['type'] === 'Annuel'): ?>
|
||||
<div style="font-size:0.75rem; color:#94a3b8; font-weight:normal;"><?= tr('bud_per_month_short') ?> <?= number_format($amountToAdd, 2, ',', ' ') ?>/<?= tr('bud_month_short') ?></div>
|
||||
<div style="font-size:0.75rem; color:#94a3b8;"><?= number_format($amountToAdd, 2, ',', ' ') ?>/mois</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<td style="padding:15px;">
|
||||
<span class="badge-type <?= strtolower($item['type']) ?>" style="background:#e2e8f0; padding:4px 8px; border-radius:12px; font-size:0.8rem; font-weight:600; color:#475569;">
|
||||
<?= tr('bud_freq_'.strtolower($item['type'])) ?>
|
||||
<span class="badge-type" style="background:#e2e8f0; padding:4px 8px; border-radius:12px; font-size:0.8rem; font-weight:600; color:#475569;">
|
||||
<?= htmlspecialchars($item['type']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td style="padding:15px; color:#64748b; font-weight:bold;"><?= $item['payment_day'] ? $item['payment_day'] : '-' ?></td>
|
||||
<td style="padding:15px; color:#64748b; font-weight:bold;"><?= $item['payment_day'] ?: '-' ?></td>
|
||||
|
||||
<td style="padding:15px;">
|
||||
<?php if ($isAutoChecked): ?>
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#f0fdf4; color:#16a34a; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #bbf7d0;">
|
||||
<span>✓</span> <?= tr('bud_state_validated') ?>
|
||||
</div>
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#f0fdf4; color:#16a34a; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #bbf7d0;"><span>✓</span> Validé</div>
|
||||
<?php elseif($hasMatchingExpense): ?>
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#fffbeb; color:#d97706; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #fde68a;">
|
||||
<span>⏳</span> <?= tr('bud_state_partial') ?>
|
||||
</div>
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#fffbeb; color:#d97706; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #fde68a;"><span>⏳</span> Partiel</div>
|
||||
<?php else: ?>
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#f8fafc; color:#94a3b8; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #e2e8f0;">
|
||||
<span>○</span> <?= tr('bud_state_waiting') ?>
|
||||
</div>
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#f8fafc; color:#94a3b8; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #e2e8f0;"><span>○</span> En attente</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<td style="padding:15px; text-align:right;">
|
||||
<div class="action-buttons" style="display:flex; gap:5px; justify-content:flex-end;">
|
||||
<button class="btn-icon-action edit" onclick='editRecapItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="<?= tr('edit') ?>">✏️</button>
|
||||
<button class="btn-icon-action delete" onclick="deleteRecapItem(<?= $item['id'] ?>)" title="<?= tr('delete') ?>">🗑️</button>
|
||||
<button class="btn-icon-action edit" onclick='editRecapItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="Modifier">✏️</button>
|
||||
<button class="btn-icon-action delete" onclick="deleteRecapItem(<?= $item['id'] ?>)" title="Supprimer">🗑️</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -189,34 +227,30 @@ $totalRevenus = 0;
|
||||
</tbody>
|
||||
<tfoot style="background:#f8fafc;">
|
||||
<tr>
|
||||
<td colspan="1" style="padding:15px;"><strong><?= tr('bud_total_income_smoothed') ?></strong></td>
|
||||
<td colspan="1" style="padding:15px;"><strong>Total Revenus Lissés</strong></td>
|
||||
<td colspan="5" style="padding:15px; color:#10b981;"><strong>+ <?= number_format($totalRevenus, 2, ',', ' ') ?> €</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1" style="padding:15px;"><strong><?= tr('bud_total_expenses_smoothed') ?></strong></td>
|
||||
<td colspan="1" style="padding:15px;"><strong>Total Dépenses & Estimations</strong></td>
|
||||
<td colspan="5" style="padding:15px; color:#ef4444;"><strong>- <?= number_format($totalDepenses, 2, ',', ' ') ?> €</strong></td>
|
||||
</tr>
|
||||
<tr style="border-top: 2px solid #e2e8f0; background: white;">
|
||||
<td colspan="1" style="padding:15px; font-size:1.1rem;"><strong><?= tr('bud_theoretical_balance_recap') ?></strong></td>
|
||||
<td colspan="1" style="padding:15px; font-size:1.1rem;"><strong>Reste à Vivre (Équilibre)</strong></td>
|
||||
<?php $balance = $totalRevenus - $totalDepenses; ?>
|
||||
<td colspan="5" style="padding:15px; font-size: 1.3em;" class="<?= $balance >= 0 ? 'text-success' : 'text-danger' ?>">
|
||||
<strong style="color:<?= $balance >= 0 ? '#10b981' : '#ef4444' ?>;"><?= number_format($balance, 2, ',', ' ') ?> € / <?= tr('bud_month_short') ?></strong>
|
||||
<td colspan="5" style="padding:15px; font-size: 1.3em;">
|
||||
<strong style="color:<?= $balance >= 0 ? '#10b981' : '#ef4444' ?>;"><?= number_format($balance, 2, ',', ' ') ?> € / mois</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="budget-note" style="margin-top:15px; font-size:0.85rem; color:#64748b;">
|
||||
<p>* <?= tr('bud_recap_footer_note') ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="budgetRecapModal" class="pf-modal" style="display:none; position:fixed; inset:0; z-index:9999; background:rgba(15, 23, 42, 0.6); backdrop-filter:blur(4px); align-items:center; justify-content:center;">
|
||||
<div class="pf-modal-content" style="background:white; width:95%; max-width:500px; border-radius:20px; box-shadow:0 20px 25px -5px rgba(0,0,0,0.1); padding:30px; position:relative;">
|
||||
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||||
<h3 id="recapModalTitle" style="margin:0; font-size:1.2rem; color:#1e293b;"><?= tr('bud_recap_modal_add') ?></h3>
|
||||
<h3 id="recapModalTitle" style="margin:0; font-size:1.2rem; color:#1e293b;">Ajouter une ligne</h3>
|
||||
<button type="button" onclick="closeRecapModal()" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">×</button>
|
||||
</div>
|
||||
|
||||
@@ -225,90 +259,66 @@ $totalRevenus = 0;
|
||||
<input type="hidden" name="id" id="item_id">
|
||||
|
||||
<div class="form-group" style="margin-bottom:15px;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_name') ?></label>
|
||||
<input type="text" name="name" id="item_name" required class="pf-input" placeholder="<?= tr('bud_ph_item_name') ?>" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom:15px;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_keywords') ?></label>
|
||||
<input type="text" name="mapping_keywords" id="item_keywords" class="pf-input" placeholder="<?= tr('bud_ph_keywords') ?>" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:#f0f9ff; border-color:#bae6fd;">
|
||||
<small style="color:#64748b; font-size:0.75rem;"><?= tr('bud_help_keywords') ?></small>
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Nom de la ligne</label>
|
||||
<input type="text" name="name" id="item_name" required class="pf-input" placeholder="ex: Assurance Auto, Estimation Courses..." style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:15px; margin-bottom:15px;">
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_amount_eur') ?></label>
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Montant Prévu (€)</label>
|
||||
<input type="number" step="0.01" name="amount" id="item_amount" required class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_day') ?></label>
|
||||
<input type="number" min="1" max="31" name="payment_day" id="item_day" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Type de Dépense</label>
|
||||
<select name="is_estimate" id="item_is_estimate" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
||||
<option value="0">Fixe (Facture, Prélèvement)</option>
|
||||
<option value="1">Variable (Estimation, Enveloppe)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:15px; margin-bottom:15px;">
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_category') ?></label>
|
||||
<select name="category" id="item_category" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
||||
<option value="expense"><?= tr('bud_cat_expense') ?></option>
|
||||
<option value="income"><?= tr('bud_cat_income') ?></option>
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">
|
||||
Catégorie(s) Cible(s) <small style="font-weight:normal;">(Maintenez Ctrl/Cmd pour choix multiple)</small>
|
||||
</label>
|
||||
<select name="category_multi[]" id="item_category" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white; min-height: 120px;" multiple required>
|
||||
<?php foreach ($dbCategories as $c): ?>
|
||||
<?php if(strtolower($c['type']) === 'expense'): ?>
|
||||
<option value="<?= htmlspecialchars($c['code']) ?>">
|
||||
<?= htmlspecialchars($c['icon'] . ' ' . $c['label']) ?>
|
||||
</option>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_frequency') ?></label>
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Fréquence</label>
|
||||
<select name="type" id="item_type" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
||||
<option value="Mensuel"><?= tr('bud_freq_mensuel') ?></option>
|
||||
<option value="Annuel"><?= tr('bud_freq_annuel') ?></option>
|
||||
<option value="Mensuel">Mensuel</option>
|
||||
<option value="Annuel">Annuel</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:15px; margin-bottom:25px;">
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_amount_type') ?></label>
|
||||
<select name="is_estimate" id="item_is_estimate" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
||||
<option value="0"><?= tr('bud_type_fixed') ?></option>
|
||||
<option value="1"><?= tr('bud_type_variable') ?></option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_regularization') ?></label>
|
||||
<select name="reg_month" id="item_reg_month" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
||||
<option value=""><?= tr('bud_reg_none') ?></option>
|
||||
<?php
|
||||
foreach($moisFr as $index => $m) {
|
||||
if($index == 0) continue;
|
||||
echo "<option value='$m'>" . tr('month_'.str_pad($index, 2, '0', STR_PAD_LEFT)) . "</option>";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:15px;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Mots-clés (Reconnaissance Bancaire)</label>
|
||||
<input type="text" name="mapping_keywords" id="item_keywords" class="pf-input" placeholder="ex: NETFLIX, DOMOFINANCE..." style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:#f0f9ff; border-color:#bae6fd;">
|
||||
<small style="color:#64748b; font-size:0.75rem;">Optionnel. Permet au système de lier automatiquement un import CSV à cette ligne.</small>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; justify-content:flex-end; gap:10px;">
|
||||
<button type="button" onclick="closeRecapModal()" class="pf-btn btn-secondary" style="width:auto; margin:0; background:#f1f5f9; color:#475569; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;"><?= tr('btn_cancel') ?></button>
|
||||
<button type="submit" class="pf-btn" style="width:auto; margin:0; background:#2563eb; color:white; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;"><?= tr('btn_save') ?></button>
|
||||
<div style="display:flex; justify-content:flex-end; gap:10px; margin-top: 25px;">
|
||||
<button type="button" onclick="closeRecapModal()" class="pf-btn btn-secondary" style="width:auto; margin:0; background:#f1f5f9; color:#475569; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;">Annuler</button>
|
||||
<button type="submit" class="pf-btn" style="width:auto; margin:0; background:#2563eb; color:white; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// --- 1. SÉCURISATION TRADUCTIONS ET LANGUE ---
|
||||
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
||||
window.I18N = {
|
||||
...(window.I18N || {}),
|
||||
'bud_recap_modal_add': <?= json_encode(tr('bud_recap_modal_add')) ?>,
|
||||
'bud_recap_modal_edit': <?= json_encode(tr('bud_recap_modal_edit')) ?>,
|
||||
'bud_recap_confirm_delete': <?= json_encode(tr('bud_recap_confirm_delete')) ?>,
|
||||
'bud_err_delete': <?= json_encode(tr('bud_err_delete')) ?>,
|
||||
'bud_err_tech': <?= json_encode(tr('bud_err_tech')) ?>,
|
||||
'bud_saving': <?= json_encode(tr('bud_sav_saving') ?? 'Sauvegarde...') ?>
|
||||
};
|
||||
|
||||
function openRecapModal(mode) {
|
||||
if (mode === "add") {
|
||||
document.getElementById("recapModalTitle").innerText = window.I18N['bud_recap_modal_add'] || 'Ajouter';
|
||||
document.getElementById("recapModalTitle").innerText = 'Ajouter une ligne';
|
||||
document.getElementById("item_id").value = "";
|
||||
document.getElementById("recapForm").reset();
|
||||
}
|
||||
@@ -322,113 +332,65 @@ function closeRecapModal() {
|
||||
}
|
||||
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('budgetRecapModal');
|
||||
if (event.target == modal) {
|
||||
closeRecapModal();
|
||||
}
|
||||
if (event.target == document.getElementById('budgetRecapModal')) closeRecapModal();
|
||||
}
|
||||
|
||||
function editRecapItem(item) {
|
||||
const data = typeof item === "string" ? JSON.parse(item) : item;
|
||||
|
||||
document.getElementById("recapModalTitle").innerText = (window.I18N['bud_recap_modal_edit'] || 'Editer') + " : " + data.name;
|
||||
document.getElementById("recapModalTitle").innerText = "Editer : " + data.name;
|
||||
document.getElementById("item_id").value = data.id;
|
||||
document.getElementById("item_name").value = data.name;
|
||||
document.getElementById("item_keywords").value = data.mapping_keywords || '';
|
||||
document.getElementById("item_amount").value = Math.abs(data.amount);
|
||||
document.getElementById("item_category").value = data.category;
|
||||
const selectedCats = data.category ? data.category.split(',') : [];
|
||||
const selectEl = document.getElementById("item_category");
|
||||
Array.from(selectEl.options).forEach(opt => {
|
||||
opt.selected = selectedCats.includes(opt.value);
|
||||
});
|
||||
document.getElementById("item_type").value = data.type;
|
||||
document.getElementById("item_day").value = data.payment_day;
|
||||
document.getElementById("item_reg_month").value = data.reg_month || "";
|
||||
document.getElementById("item_is_estimate").value = data.is_estimate;
|
||||
|
||||
document.getElementById("budgetRecapModal").style.display = "flex";
|
||||
document.body.classList.add('no-scroll');
|
||||
}
|
||||
|
||||
// --- 2. INTERCEPTION ASYNCHRONE DU FORMULAIRE ---
|
||||
const recapForm = document.getElementById('recapForm');
|
||||
if (recapForm) {
|
||||
recapForm.addEventListener('submit', async function(e) {
|
||||
document.getElementById('recapForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const submitBtn = this.querySelector('button[type="submit"]');
|
||||
const originalText = submitBtn.innerText;
|
||||
submitBtn.innerText = window.I18N['bud_saving'] || '⏳ ...';
|
||||
submitBtn.disabled = true;
|
||||
const btn = this.querySelector('button[type="submit"]');
|
||||
btn.disabled = true; btn.innerText = '⏳ ...';
|
||||
|
||||
const formData = new FormData(this);
|
||||
|
||||
const cats = formData.getAll('category_multi[]');
|
||||
formData.delete('category_multi[]');
|
||||
formData.append('category', cats.join(','));
|
||||
|
||||
formData.append('ajax', '1');
|
||||
|
||||
// 💡 Utilisation sécurisée de getAttribute
|
||||
const actionUrl = this.getAttribute('action');
|
||||
const finalUrl = actionUrl.startsWith('/') ? actionUrl.substring(1) : actionUrl;
|
||||
const url = this.getAttribute('action');
|
||||
|
||||
try {
|
||||
const response = await fetch(finalUrl, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
// 💡 Lecture robuste (anti-Warnings PHP)
|
||||
const textResult = await response.text();
|
||||
try {
|
||||
const result = JSON.parse(textResult);
|
||||
if (result.success) {
|
||||
closeRecapModal();
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert((window.I18N['bud_err_tech'] || 'Erreur') + " : " + (result.error || "Inconnue"));
|
||||
}
|
||||
} catch (jsonError) {
|
||||
console.error("Réponse non-JSON :", textResult);
|
||||
alert("Le serveur a renvoyé une erreur PHP. Regarde la console (F12).");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur Fetch:", error);
|
||||
alert(window.I18N['bud_err_tech'] || 'Erreur technique');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerText = originalText;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- 3. SUPPRESSION ASYNCHRONE ---
|
||||
async function deleteRecapItem(id) {
|
||||
if (!confirm(window.I18N['bud_recap_confirm_delete'] || "Confirmer la suppression ?")) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("action", "delete");
|
||||
formData.append("id", id);
|
||||
formData.append("ajax", "1"); // Signale à l'API qu'on attend du JSON
|
||||
|
||||
try {
|
||||
const response = await fetch("modules/budget/includes/api/manage-item.php", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
const textResult = await response.text();
|
||||
try {
|
||||
const result = JSON.parse(textResult);
|
||||
// On s'assure que si l'API ne renvoie pas success, on le signale
|
||||
if (result.success !== false) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert((window.I18N['bud_err_delete'] || 'Erreur de suppression') + " : " + (result.error || ""));
|
||||
}
|
||||
} catch (jsonErr) {
|
||||
console.error("Réponse non-JSON lors de la suppression :", textResult);
|
||||
window.location.reload(); // Fallback si l'API redirige au lieu de répondre en JSON
|
||||
}
|
||||
const res = await fetch(url.startsWith('/') ? url.substring(1) : url, { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.success) window.location.reload();
|
||||
else alert("Erreur : " + data.error);
|
||||
} catch (err) {
|
||||
console.error("Erreur réseau Suppression:", err);
|
||||
alert(window.I18N['bud_err_delete'] || 'Erreur réseau');
|
||||
alert("Erreur technique lors de la sauvegarde.");
|
||||
} finally {
|
||||
btn.disabled = false; btn.innerText = 'Enregistrer';
|
||||
}
|
||||
});
|
||||
|
||||
async function deleteRecapItem(id) {
|
||||
if (!confirm("Confirmer la suppression ?")) return;
|
||||
const formData = new FormData();
|
||||
formData.append("action", "delete"); formData.append("id", id); formData.append("ajax", "1");
|
||||
try {
|
||||
const res = await fetch("modules/budget/includes/api/manage-item.php", { method: "POST", body: formData });
|
||||
const data = await res.json();
|
||||
if (data.success !== false) window.location.reload();
|
||||
else alert("Erreur : " + data.error);
|
||||
} catch (err) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,523 @@
|
||||
<?php
|
||||
// Sécurité : Ce fichier ne doit pas être appelé directement
|
||||
if (!defined('CURRENCY')) {
|
||||
define('CURRENCY', '€');
|
||||
}
|
||||
?>
|
||||
<div class="budget-settings-backdrop" id="modal-budget-settings">
|
||||
<div class="budget-settings-modal">
|
||||
<div class="bs-header">
|
||||
<h3>⚙️ <?= tr('budget_settings_title') ?></h3>
|
||||
<button class="bs-close" onclick="closeBudgetSettings()">×</button>
|
||||
</div>
|
||||
<div class="bs-layout">
|
||||
<div class="bs-sidebar">
|
||||
<button class="bs-tab-btn active" onclick="switchBsTab('accounts', this)">
|
||||
<?= tr('bs_tab_accounts') ?>
|
||||
<?php if (!$hasAccounts): ?><span class="alert-dot alert-dot-inline">!</span><?php endif; ?>
|
||||
</button>
|
||||
<button class="bs-tab-btn" onclick="switchBsTab('categories', this)">
|
||||
<?= tr('bs_tab_categories') ?>
|
||||
<?php if (!$hasCategories): ?><span class="alert-dot alert-dot-inline">!</span><?php endif; ?>
|
||||
</button>
|
||||
<button class="bs-tab-btn" onclick="switchBsTab('rules', this)"><?= tr('bs_tab_rules') ?></button>
|
||||
<button class="bs-tab-btn" onclick="switchBsTab('salaries', this)">
|
||||
<?= tr('bs_tab_salaries') ?>
|
||||
<?php if (!$hasSalaries): ?><span class="alert-dot alert-dot-inline">!</span><?php endif; ?>
|
||||
</button>
|
||||
<button class="bs-tab-btn" onclick="switchBsTab('csv', this)"><?= tr('bs_tab_csv') ?></button>
|
||||
</div>
|
||||
|
||||
<div class="bs-content">
|
||||
|
||||
<?php if (!isset($isBudgetSetupOk) || !$isBudgetSetupOk || !$hasBudgetItems): ?>
|
||||
<div class="bs-onboarding-card">
|
||||
<h4 class="bs-onboarding-title">
|
||||
<span>🚀</span> <?= tr('bs_onb_title') ?>
|
||||
</h4>
|
||||
<p class="bs-onboarding-desc"><?= tr('bs_onb_desc') ?></p>
|
||||
<ul class="bs-onboarding-list">
|
||||
<li>
|
||||
<strong class="<?= $hasCategories ? 'text-success' : 'text-danger' ?>"><?= $hasCategories ? '✅' : '❌' ?></strong>
|
||||
<a class="bs-onboarding-link" onclick="switchBsTab('categories', document.querySelectorAll('.bs-tab-btn')[1]); return false;"><?= tr('bs_onb_cat_link') ?></a> : <?= tr('bs_onb_cat_desc') ?>
|
||||
</li>
|
||||
<li>
|
||||
<strong class="<?= $hasAccounts ? 'text-success' : 'text-danger' ?>"><?= $hasAccounts ? '✅' : '❌' ?></strong>
|
||||
<a class="bs-onboarding-link" onclick="switchBsTab('accounts', document.querySelectorAll('.bs-tab-btn')[0]); return false;"><?= tr('bs_onb_acc_link') ?></a> : <?= tr('bs_onb_acc_desc') ?>
|
||||
</li>
|
||||
<li>
|
||||
<strong class="<?= $hasSalaries ? 'text-success' : 'text-danger' ?>"><?= $hasSalaries ? '✅' : '❌' ?></strong>
|
||||
<a class="bs-onboarding-link" onclick="switchBsTab('salaries', document.querySelectorAll('.bs-tab-btn')[3]); return false;"><?= tr('bs_onb_sal_link') ?></a> : <?= tr('bs_onb_sal_desc') ?>
|
||||
</li>
|
||||
<li style="margin-top: 8px; padding-top: 8px; border-top: 1px dashed rgba(180, 83, 9, 0.3);">
|
||||
<strong class="<?= $hasBudgetItems ? 'text-success' : 'text-warning' ?>" style="color: <?= $hasBudgetItems ? '' : '#f59e0b' ?>"><?= $hasBudgetItems ? '✅' : '⏳' ?></strong>
|
||||
<a href="?tab=recap" onclick="closeBudgetSettings();" class="bs-onboarding-link"><?= tr('bs_onb_items_link') ?></a> : <?= tr('bs_onb_items_desc') ?>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div id="pane-accounts" class="bs-pane active">
|
||||
<div class="bs-pane-header">
|
||||
<h4 class="bs-section-title"><?= tr('bs_tab_accounts') ?></h4>
|
||||
<div class="bs-currency-selector">
|
||||
<span><?= tr('bs_currency_label') ?> :</span>
|
||||
<select id="bs-currency-select" class="pf-input bs-currency-select" onchange="updateBudgetCurrency(this)">
|
||||
<option value="€">EUR (€)</option>
|
||||
<option value="$">USD ($)</option>
|
||||
<option value="£">GBP (£)</option>
|
||||
<option value="CHF">CHF</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="bs-list-accounts" class="bs-list-container">⏳ <?= tr('loading') ?></div>
|
||||
<hr class="bs-divider">
|
||||
<h5 class="bs-section-subtitle"><?= tr('bs_add_account_title') ?></h5>
|
||||
<form id="form-add-account" class="bs-form-inline">
|
||||
<input type="text" name="name" class="pf-input bs-input-flex" placeholder="<?= tr('bs_account_name_ph') ?>" required>
|
||||
<select name="type" class="pf-input bs-input-fixed">
|
||||
<option value="checking"><?= tr('bs_type_checking') ?></option>
|
||||
<option value="savings"><?= tr('bs_type_savings') ?></option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-secondary"><?= tr('btn_add') ?></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="pane-categories" class="bs-pane">
|
||||
<h4 class="bs-section-title"><?= tr('bs_tab_categories') ?></h4>
|
||||
<div id="bs-list-categories" class="bs-list-container">⏳ <?= tr('loading') ?></div>
|
||||
<hr class="bs-divider">
|
||||
<h5 class="bs-section-subtitle"><?= tr('bs_add_category_title') ?></h5>
|
||||
<form id="form-add-category" class="bs-form-inline align-center">
|
||||
<input type="text" name="code" class="pf-input bs-input-fixed" placeholder="<?= tr('bs_cat_code_ph') ?>" required>
|
||||
<input type="text" name="label" class="pf-input bs-input-flex" placeholder="<?= tr('bs_cat_label_ph') ?>" required>
|
||||
|
||||
<select name="type" class="pf-input bs-input-fixed">
|
||||
<option value="Expense"><?= tr('bs_type_expense') ?></option>
|
||||
<option value="Income"><?= tr('bs_type_income') ?></option>
|
||||
<option value="Savings"><?= tr('bs_type_savings_cat') ?></option>
|
||||
</select>
|
||||
|
||||
<select name="icon" class="pf-input bs-input-icon">
|
||||
<option value="📌">📌</option><option value="🛒">🛒</option><option value="🥖">🥖</option>
|
||||
<option value="🍽️">🍽️</option><option value="⛽">⛽</option><option value="🚗">🚗</option>
|
||||
<option value="🚆">🚆</option><option value="🏠">🏠</option><option value="⚡">⚡</option>
|
||||
<option value="💧">💧</option><option value="⚕️">⚕️</option><option value="🎒">🎒</option>
|
||||
<option value="👕">👕</option><option value="📱">📱</option><option value="🎮">🎮</option>
|
||||
<option value="✈️">✈️</option><option value="🎁">🎁</option><option value="🐶">🐶</option>
|
||||
<option value="💵">💵</option><option value="🐷">🐷</option><option value="📈">📈</option>
|
||||
</select>
|
||||
|
||||
<input type="color" name="color" value="#3b82f6" class="bs-input-color">
|
||||
<button type="submit" class="btn btn-secondary"><?= tr('btn_add') ?></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="pane-rules" class="bs-pane">
|
||||
<h4 class="bs-section-title"><?= tr('bs_tab_rules') ?></h4>
|
||||
<input type="text" id="input-search-rules" class="bs-rule-search" placeholder="<?= tr('bs_rule_search_ph') ?>" onkeyup="filterRules()">
|
||||
<div id="bs-list-rules" class="bs-list-container">⏳ <?= tr('loading') ?></div>
|
||||
<hr class="bs-divider">
|
||||
<h5 class="bs-section-subtitle"><?= tr('bs_add_rule_title') ?></h5>
|
||||
<form id="form-add-rule" class="bs-form-inline align-center">
|
||||
<input type="text" name="keyword" class="pf-input bs-input-flex" placeholder="<?= tr('bs_rule_keyword_ph') ?>" required>
|
||||
<select name="category" id="select-rule-category" class="pf-input bs-input-fixed" required></select>
|
||||
<button type="submit" class="btn btn-secondary"><?= tr('btn_add') ?></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="pane-salaries" class="bs-pane">
|
||||
<h4 class="bs-section-title"><?= tr('bs_tab_salaries') ?> (<?= date('Y') ?>)</h4>
|
||||
<div id="bs-list-salaries" class="bs-list-container">⏳ <?= tr('loading') ?></div>
|
||||
</div>
|
||||
|
||||
<div id="pane-csv" class="bs-pane">
|
||||
<h4 class="bs-section-title"><?= tr('bs_csv_title') ?></h4>
|
||||
<p class="pf-muted-tiny bs-desc"><?= tr('bs_csv_desc') ?></p>
|
||||
|
||||
<div id="csv-drop-zone" class="bs-csv-dropzone">
|
||||
<div class="bs-csv-icon">📥</div>
|
||||
<h5 class="bs-csv-title"><?= tr('bs_csv_drop_title') ?></h5>
|
||||
<p class="pf-muted-tiny bs-csv-subtitle"><?= tr('bs_csv_drop_desc') ?></p>
|
||||
<input type="file" id="csv_file_input" accept=".csv" style="display: none;">
|
||||
</div>
|
||||
|
||||
<div id="csv-preview-container" class="bs-csv-preview"></div>
|
||||
|
||||
<h5 class="bs-section-subtitle bordered"><?= tr('bs_csv_settings_title') ?></h5>
|
||||
<form id="form-csv-mapping" onsubmit="saveCsvMapping(event)">
|
||||
<div class="bs-grid-2">
|
||||
<div>
|
||||
<label class="pf-label"><?= tr('bs_csv_delimiter') ?></label>
|
||||
<select name="csv_delimiter" id="csv_delimiter" class="pf-input">
|
||||
<option value=";"><?= tr('bs_csv_delim_semi') ?></option>
|
||||
<option value=","><?= tr('bs_csv_delim_comma') ?></option>
|
||||
<option value="\t"><?= tr('bs_csv_delim_tab') ?></option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="pf-label"><?= tr('bs_csv_date_format') ?></label>
|
||||
<select name="csv_date_format" id="csv_date_format" class="pf-input">
|
||||
<option value="d/m/Y">JJ/MM/AAAA</option>
|
||||
<option value="Y-m-d">AAAA-MM-JJ</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="bs-section-subtitle">
|
||||
<?= tr('bs_csv_col_index') ?>
|
||||
<small class="bs-text-muted-normal"><?= tr('bs_csv_col_index_help') ?></small>
|
||||
</h5>
|
||||
<div class="bs-grid-2">
|
||||
<div>
|
||||
<label class="pf-label"><?= tr('bs_csv_col_date') ?></label>
|
||||
<input type="number" min="0" name="csv_col_date" id="csv_col_date" class="pf-input" value="0" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="pf-label"><?= tr('bs_csv_col_label') ?></label>
|
||||
<input type="number" min="0" name="csv_col_label" id="csv_col_label" class="pf-input" value="1" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bs-card-gray">
|
||||
<label class="pf-label"><?= tr('bs_csv_amount_mgmt') ?></label>
|
||||
<select name="csv_amount_type" id="csv_amount_type" class="pf-input bs-input-mb" onchange="toggleCsvAmountCols(this.value)">
|
||||
<option value="single"><?= tr('bs_csv_amount_single') ?></option>
|
||||
<option value="split"><?= tr('bs_csv_amount_split') ?></option>
|
||||
</select>
|
||||
|
||||
<div class="bs-grid-2">
|
||||
<div>
|
||||
<label class="pf-label" id="lbl_col_debit"><?= tr('bs_csv_col_amount') ?></label>
|
||||
<input type="number" min="0" name="csv_col_debit" id="csv_col_debit" class="pf-input" value="8" required>
|
||||
</div>
|
||||
<div id="wrapper_col_credit" style="display:none;">
|
||||
<label class="pf-label"><?= tr('bs_csv_col_credit') ?></label>
|
||||
<input type="number" min="0" name="csv_col_credit" id="csv_col_credit" class="pf-input" value="9">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bs-input-mb">
|
||||
<label class="pf-label"><?= tr('bs_csv_col_ref') ?></label>
|
||||
<input type="number" min="0" name="csv_col_ref" id="csv_col_ref" class="pf-input" value="3">
|
||||
</div>
|
||||
<div class="bs-text-right">
|
||||
<button type="submit" class="btn btn-secondary">💾 <?= tr('btn_save_format') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const tabsContainer = document.querySelector('.budget-tabs-container');
|
||||
const activeTab = document.querySelector('.tab-item.active');
|
||||
if (tabsContainer && activeTab) {
|
||||
if (tabsContainer.scrollWidth > tabsContainer.clientWidth) {
|
||||
const scrollPos = activeTab.offsetLeft - (tabsContainer.offsetWidth / 2) + (activeTab.offsetWidth / 2);
|
||||
tabsContainer.scrollTo({ left: scrollPos, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function openBudgetSettings() {
|
||||
document.getElementById('modal-budget-settings').classList.add('show');
|
||||
document.body.classList.add('no-scroll');
|
||||
loadBudgetSettingsData();
|
||||
}
|
||||
|
||||
function closeBudgetSettings() {
|
||||
document.getElementById('modal-budget-settings').classList.remove('show');
|
||||
document.body.classList.remove('no-scroll');
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
document.getElementById('modal-budget-settings')?.addEventListener('click', (e) => {
|
||||
if (e.target.id === 'modal-budget-settings') closeBudgetSettings();
|
||||
});
|
||||
|
||||
function switchBsTab(tabId, btnEl) {
|
||||
document.querySelectorAll('.bs-tab-btn').forEach(b => b.classList.remove('active'));
|
||||
document.querySelectorAll('.bs-pane').forEach(p => p.classList.remove('active'));
|
||||
btnEl.classList.add('active');
|
||||
document.getElementById('pane-' + tabId).classList.add('active');
|
||||
}
|
||||
|
||||
async function loadBudgetSettingsData() {
|
||||
try {
|
||||
const response = await pachaFetch('/modules/budget/includes/api/settings.php?action=get_all', { method: 'GET' });
|
||||
if (!response.success) throw new Error(response.error || tr('error_occured'));
|
||||
renderAccounts(response.data.accounts);
|
||||
renderCategories(response.data.categories);
|
||||
populateRuleCategories(response.data.categories);
|
||||
renderRules(response.data.rules);
|
||||
renderSalaries(response.data.salaries, response.data.year);
|
||||
const currencySelect = document.getElementById('bs-currency-select');
|
||||
if (currencySelect && response.data.currency) currencySelect.value = response.data.currency;
|
||||
} catch (err) {
|
||||
console.error("Erreur chargement paramètres :", err);
|
||||
const errorMsg = `<div class="pf-alert pf-alert--error bs-input-mb">❌ ${tr('error_loading_settings')} : ${err.message}</div>`;
|
||||
['accounts', 'categories', 'rules', 'salaries'].forEach(id => {
|
||||
const el = document.getElementById('bs-list-' + id);
|
||||
if (el) el.innerHTML = errorMsg;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 1. ONGLET : COMPTES
|
||||
function renderAccounts(accounts) {
|
||||
const container = document.getElementById('bs-list-accounts');
|
||||
if (!container) return;
|
||||
if (!accounts || accounts.length === 0) {
|
||||
container.innerHTML = `<em class="text-muted">${tr('bs_empty_accounts')}</em>`;
|
||||
return;
|
||||
}
|
||||
let html = '';
|
||||
accounts.forEach(acc => {
|
||||
const typeBadge = acc.account_type === 'checking' ? tr('bs_type_checking') : tr('bs_type_savings');
|
||||
const defaultBadge = acc.is_default == 1 ? `<span class="bs-badge-default">${tr('bs_badge_default')}</span>` : '';
|
||||
const ownerText = acc.owner_name ? `${tr('bs_owner')} : ${acc.owner_name}` : tr('bs_shared_account');
|
||||
html += `
|
||||
<div class="bs-list-item">
|
||||
<div>
|
||||
<strong>${acc.name}</strong> ${defaultBadge}<br>
|
||||
<small class="text-muted">${typeBadge} · ${ownerText}</small>
|
||||
</div>
|
||||
<div class="pf-flex-gap-8">
|
||||
<button type="button" class="btn-icon-action delete" onclick="deleteAccount(${acc.id})" title="${tr('btn_delete')}">🗑️</button>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// 2. ONGLET : CATÉGORIES
|
||||
function renderCategories(categories) {
|
||||
const container = document.getElementById('bs-list-categories');
|
||||
if (!container) return;
|
||||
if (!categories || categories.length === 0) {
|
||||
container.innerHTML = `<em class="text-muted">${tr('bs_empty_categories')}</em>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Tri JS pour respecter l'ordre au chargement de la modale
|
||||
categories.sort((a, b) => {
|
||||
if (a.type !== b.type) return a.type === 'Income' ? -1 : 1;
|
||||
return (a.sort_order || 0) - (b.sort_order || 0);
|
||||
});
|
||||
|
||||
let html = '';
|
||||
categories.forEach(cat => {
|
||||
let typeLabel = cat.type === 'Income' ? tr('bs_type_income') : cat.type === 'Expense' ? tr('bs_type_expense') : tr('bs_type_savings_cat');
|
||||
html += `
|
||||
<div class="bs-list-item" data-id="${cat.id}" style="border-left: 4px solid ${cat.color || '#ccc'};">
|
||||
<div>
|
||||
<span style="font-size:1.2rem; margin-right:8px;">${cat.icon || '📌'}</span>
|
||||
<strong>${cat.label}</strong> <small class="text-muted">(${cat.code} — ${typeLabel})</small>
|
||||
</div>
|
||||
<div class="pf-flex-gap-8">
|
||||
<button type="button" class="btn-icon-action" onclick="moveCatUp(this)" title="Monter">⬆️</button>
|
||||
<button type="button" class="btn-icon-action" onclick="moveCatDown(this)" title="Descendre">⬇️</button>
|
||||
<button type="button" class="btn-icon-action delete" onclick="deleteCategory(${cat.id})" title="${tr('btn_delete')}">🗑️</button>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// Nouvelles fonctions pour réordonner visuellement et sauvegarder
|
||||
function moveCatUp(btn) {
|
||||
let row = btn.closest('.bs-list-item');
|
||||
if (row.previousElementSibling) {
|
||||
row.parentNode.insertBefore(row, row.previousElementSibling);
|
||||
saveCategoriesOrder();
|
||||
}
|
||||
}
|
||||
|
||||
function moveCatDown(btn) {
|
||||
let row = btn.closest('.bs-list-item');
|
||||
if (row.nextElementSibling) {
|
||||
row.parentNode.insertBefore(row.nextElementSibling, row);
|
||||
saveCategoriesOrder();
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCategoriesOrder() {
|
||||
let ids = Array.from(document.querySelectorAll('#bs-list-categories .bs-list-item')).map(el => el.dataset.id);
|
||||
let fd = new FormData();
|
||||
fd.append('action', 'save_categories_order');
|
||||
fd.append('order', JSON.stringify(ids));
|
||||
try {
|
||||
await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd });
|
||||
} catch (err) {
|
||||
console.error("Erreur de sauvegarde de l'ordre :", err);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. ONGLET : RÈGLES D'IMPORT
|
||||
function renderRules(rules) {
|
||||
const container = document.getElementById('bs-list-rules');
|
||||
if (!container) return;
|
||||
if (!rules || rules.length === 0) {
|
||||
container.innerHTML = `<em class="text-muted">${tr('bs_empty_rules')}</em>`;
|
||||
return;
|
||||
}
|
||||
const groupedRules = {};
|
||||
rules.forEach(r => {
|
||||
const catName = r.cat_label || r.category;
|
||||
if (!groupedRules[catName]) groupedRules[catName] = [];
|
||||
groupedRules[catName].push(r);
|
||||
});
|
||||
let html = '';
|
||||
for (const [catName, catRules] of Object.entries(groupedRules)) {
|
||||
html += `
|
||||
<details class="pf-accordion js-rule-group" data-catname="${catName.toLowerCase()}">
|
||||
<summary class="pf-accordion-summary bs-accordion-summary">
|
||||
<span>📁 ${catName}</span>
|
||||
<span class="bs-accordion-count">${catRules.length}</span>
|
||||
</summary>
|
||||
<div class="pf-accordion-content" style="padding: 0.5rem;">
|
||||
<div class="bs-rule-tags">`;
|
||||
catRules.forEach(r => {
|
||||
html += `<div class="bs-rule-tag js-rule-tag" data-keyword="${r.keyword.toLowerCase()}">
|
||||
${r.keyword}
|
||||
<button type="button" class="bs-rule-tag-del" onclick="deleteRule(${r.id})" title="${tr('btn_delete')}">×</button>
|
||||
</div>`;
|
||||
});
|
||||
html += ` </div>
|
||||
</div>
|
||||
</details>`;
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function filterRules() {
|
||||
const searchVal = document.getElementById('input-search-rules').value.toLowerCase();
|
||||
document.querySelectorAll('.js-rule-group').forEach(group => {
|
||||
let hasVisibleTag = false;
|
||||
const catName = group.getAttribute('data-catname');
|
||||
group.querySelectorAll('.js-rule-tag').forEach(tag => {
|
||||
const keyword = tag.getAttribute('data-keyword');
|
||||
if (keyword.includes(searchVal)) { tag.style.display = 'inline-flex'; hasVisibleTag = true; }
|
||||
else { tag.style.display = 'none'; }
|
||||
});
|
||||
if (catName.includes(searchVal) && searchVal.length > 0) {
|
||||
group.querySelectorAll('.js-rule-tag').forEach(tag => tag.style.display = 'inline-flex');
|
||||
hasVisibleTag = true;
|
||||
}
|
||||
if (hasVisibleTag) {
|
||||
group.style.display = 'block';
|
||||
if (searchVal.length > 0) group.setAttribute('open', '');
|
||||
} else {
|
||||
group.style.display = 'none'; group.removeAttribute('open');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 5. ONGLET : SALAIRES
|
||||
function renderSalaries(salaries, year) {
|
||||
const container = document.getElementById('bs-list-salaries');
|
||||
if (!container) return;
|
||||
if (!salaries || salaries.length === 0) {
|
||||
container.innerHTML = `<em class="text-muted">${tr('bs_empty_salaries')} ${year}.</em>`;
|
||||
return;
|
||||
}
|
||||
const currency = '<?= CURRENCY ?>';
|
||||
let html = `<p class="bs-desc">${tr('bs_salaries_year_desc')} <strong>${year}</strong>.</p>`;
|
||||
salaries.forEach(s => {
|
||||
html += `
|
||||
<div class="bs-list-item" id="salary-item-${s.id}">
|
||||
<div>
|
||||
<strong>${s.person}</strong><br>
|
||||
<small class="text-muted">
|
||||
${tr('bs_salary_net')} : ${s.salary} ${currency}<br>
|
||||
${tr('bs_salary_fees')} : ${s.mensualite} ${currency}
|
||||
</small>
|
||||
</div>
|
||||
<div class="pf-flex-gap-8">
|
||||
<button type="button" class="btn-icon-action edit" data-id="${s.id}" data-person="${s.person}" data-salary="${s.salary}" data-mensualite="${s.mensualite}" onclick="inlineEditSalary(this)" title="${tr('btn_edit')}">✏️</button>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// --- CRUD ACTIONS ---
|
||||
async function deleteAccount(id) { if (!await pachaConfirm(tr('btn_delete'), tr('bs_confirm_delete'))) return; try { const fd = new FormData(); fd.append('action', 'delete_account'); fd.append('id', id); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } }
|
||||
const formAddAccount = document.getElementById('form-add-account');
|
||||
if (formAddAccount) formAddAccount.addEventListener('submit', async (e) => { e.preventDefault(); const btn = formAddAccount.querySelector('button'); const oldText = btn.innerText; btn.innerText = '⏳'; btn.disabled = true; try { const fd = new FormData(formAddAccount); fd.append('action', 'add_account'); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); formAddAccount.reset(); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } finally { btn.innerText = oldText; btn.disabled = false; } });
|
||||
|
||||
async function deleteCategory(id) { if (!await pachaConfirm(tr('btn_delete'), tr('bs_confirm_delete_cat'))) return; try { const fd = new FormData(); fd.append('action', 'delete_category'); fd.append('id', id); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } }
|
||||
const formAddCategory = document.getElementById('form-add-category');
|
||||
if (formAddCategory) formAddCategory.addEventListener('submit', async (e) => { e.preventDefault(); const btn = formAddCategory.querySelector('button'); const oldText = btn.innerText; btn.innerText = '⏳'; btn.disabled = true; try { const fd = new FormData(formAddCategory); fd.append('action', 'add_category'); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); formAddCategory.reset(); formAddCategory.querySelector('input[type="color"]').value = "#3b82f6"; await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } finally { btn.innerText = oldText; btn.disabled = false; } });
|
||||
|
||||
function populateRuleCategories(categories) { const select = document.getElementById('select-rule-category'); if (!select) return; select.innerHTML = ''; if (!categories) return; categories.forEach(cat => { const opt = document.createElement('option'); opt.value = cat.code; opt.textContent = `${cat.icon || ''} ${cat.label}`; select.appendChild(opt); }); }
|
||||
async function deleteRule(id) { if (!await pachaConfirm(tr('btn_delete'), tr('bs_confirm_delete_rule'))) return; try { const fd = new FormData(); fd.append('action', 'delete_rule'); fd.append('id', id); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } }
|
||||
const formAddRule = document.getElementById('form-add-rule');
|
||||
if (formAddRule) formAddRule.addEventListener('submit', async (e) => { e.preventDefault(); const btn = formAddRule.querySelector('button'); const oldText = btn.innerText; btn.innerText = '⏳'; btn.disabled = true; try { const fd = new FormData(formAddRule); fd.append('action', 'add_rule'); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); formAddRule.reset(); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } finally { btn.innerText = oldText; btn.disabled = false; } });
|
||||
|
||||
function inlineEditSalary(btnEl) {
|
||||
const id = btnEl.dataset.id; const person = btnEl.dataset.person; const salary = btnEl.dataset.salary; const mensualite = btnEl.dataset.mensualite; const currency = '<?= CURRENCY ?>';
|
||||
document.getElementById(`salary-item-${id}`).innerHTML = `
|
||||
<form onsubmit="submitInlineSalary(event, ${id})" class="bs-form-inline align-center">
|
||||
<div class="bs-input-flex"><strong>${person}</strong></div>
|
||||
<div style="display:flex; align-items:center; gap:4px;"><small class="text-muted">${tr('bs_salary_net')}:</small><input type="number" step="0.01" name="salary" class="pf-input bs-input-fixed" value="${salary}" required> ${currency}</div>
|
||||
<div style="display:flex; align-items:center; gap:4px;"><small class="text-muted">${tr('bs_salary_fees')}:</small><input type="number" step="0.01" name="mensualite" class="pf-input bs-input-fixed" value="${mensualite}" required> ${currency}</div>
|
||||
<button type="submit" class="btn btn-secondary">💾</button>
|
||||
<button type="button" class="btn btn-ghost" onclick="loadBudgetSettingsData()">❌</button>
|
||||
</form>`;
|
||||
}
|
||||
async function submitInlineSalary(event, id) { event.preventDefault(); const form = event.target; const btn = form.querySelector('button'); btn.innerText = '⏳'; btn.disabled = true; try { const fd = new FormData(form); fd.append('action', 'save_salary'); fd.append('id', id); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); btn.innerText = '💾'; btn.disabled = false; } }
|
||||
|
||||
async function updateBudgetCurrency(selectEl) { try { const fd = new FormData(); fd.append('action', 'save_currency'); fd.append('currency', selectEl.value); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); if (typeof showToast === 'function') { showToast(`${tr('bs_currency_updated')} ✅`); } await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } }
|
||||
|
||||
// CSV
|
||||
function toggleCsvAmountCols(type) { if (type === 'split') { document.getElementById('wrapper_col_credit').style.display = 'block'; document.getElementById('lbl_col_debit').innerText = tr('bs_csv_col_debit'); } else { document.getElementById('wrapper_col_credit').style.display = 'none'; document.getElementById('lbl_col_debit').innerText = tr('bs_csv_col_amount'); } }
|
||||
function renderCsvMapping(mapping) { if (!mapping) return; document.getElementById('csv_delimiter').value = mapping.delimiter || ';'; document.getElementById('csv_date_format').value = mapping.date_format || 'd/m/Y'; document.getElementById('csv_col_date').value = mapping.col_date ?? 0; document.getElementById('csv_col_label').value = mapping.col_label ?? 1; document.getElementById('csv_col_ref').value = mapping.col_ref ?? 3; const amountType = mapping.amount_type || 'split'; document.getElementById('csv_amount_type').value = amountType; document.getElementById('csv_col_debit').value = mapping.col_debit ?? 8; document.getElementById('csv_col_credit').value = mapping.col_credit ?? 9; toggleCsvAmountCols(amountType); }
|
||||
async function saveCsvMapping(e) { e.preventDefault(); const form = e.target; const btn = form.querySelector('button'); const oldText = btn.innerText; btn.innerText = '⏳...'; btn.disabled = true; try { const fd = new FormData(form); fd.append('action', 'save_csv_mapping'); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); if (typeof showToast === 'function') showToast(tr('bs_csv_saved')); else alert(tr('bs_csv_saved')); } catch (err) { alert(tr('error_occured') + " : " + err.message); } finally { btn.innerText = oldText; btn.disabled = false; } }
|
||||
|
||||
const dropZone = document.getElementById('csv-drop-zone'); const fileInput = document.getElementById('csv_file_input');
|
||||
if (dropZone && fileInput) {
|
||||
dropZone.addEventListener('click', () => fileInput.click());
|
||||
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.style.backgroundColor = 'rgba(0, 123, 255, 0.05)'; dropZone.style.borderColor = 'var(--primary-color, #007bff)'; });
|
||||
dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.style.backgroundColor = ''; dropZone.style.borderColor = 'var(--border-light, #ccc)'; });
|
||||
dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.style.backgroundColor = ''; dropZone.style.borderColor = 'var(--border-light, #ccc)'; if (e.dataTransfer.files.length) { fileInput.files = e.dataTransfer.files; handleCsvUpload(e.dataTransfer.files[0]); } });
|
||||
fileInput.addEventListener('change', function() { if (this.files.length) { handleCsvUpload(this.files[0]); } });
|
||||
}
|
||||
function handleCsvUpload(file) {
|
||||
if (!file.name.endsWith('.csv')) { alert(tr('bs_csv_err_type')); return; }
|
||||
if (typeof showToast === 'function') { showToast(`${tr('bs_csv_file_ok')} : ${file.name}`); }
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const text = e.target.result; const delimiter = document.getElementById('csv_delimiter').value || ';';
|
||||
const lines = text.split(/\r?\n/).filter(line => line.trim().length > 0);
|
||||
if (lines.length > 0) {
|
||||
const headers = lines[0].split(delimiter).map(h => h.replace(/^"|"$/g, '').trim());
|
||||
const sampleRows = [];
|
||||
for (let i = 1; i < Math.min(lines.length, 3); i++) { sampleRows.push(lines[i].split(delimiter).map(d => d.replace(/^"|"$/g, '').trim())); }
|
||||
renderCsvTablePreview(headers, sampleRows);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file, 'ISO-8859-1');
|
||||
}
|
||||
function renderCsvTablePreview(headers, sampleRows) {
|
||||
const container = document.getElementById('csv-preview-container');
|
||||
let html = `<h5 class="bs-section-subtitle">👁️ ${tr('bs_csv_preview_title')}</h5>`;
|
||||
html += `<div class="bs-csv-table-wrapper"><table class="bs-csv-table"><thead class="bs-csv-thead"><tr>`;
|
||||
headers.forEach((header, index) => {
|
||||
const cleanHeader = header || `(${tr('bs_csv_empty_col')})`;
|
||||
html += `<th class="bs-csv-th"><div class="bs-csv-th-index">N° ${index}</div><div>${cleanHeader}</div></th>`;
|
||||
});
|
||||
html += `</tr></thead><tbody>`;
|
||||
if (sampleRows.length === 0) { html += `<tr><td colspan="${headers.length}" class="bs-csv-td-empty">${tr('bs_csv_no_data')}</td></tr>`; }
|
||||
else { sampleRows.forEach(row => { html += `<tr class="bs-csv-tr">`; for (let i = 0; i < headers.length; i++) { const cellData = row[i] !== undefined ? row[i] : ''; const displayData = cellData.length > 40 ? cellData.substring(0, 40) + '...' : cellData; html += `<td class="bs-csv-td">${displayData}</td>`; } html += `</tr>`; }); }
|
||||
html += `</tbody></table></div>`;
|
||||
container.innerHTML = html; container.style.display = 'block';
|
||||
}
|
||||
</script>
|
||||
+263
-113
@@ -57,19 +57,29 @@ 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)");
|
||||
$stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id, salary_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$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) {
|
||||
if (isset($line['import_check'])) {
|
||||
$cat = $line['cat'];
|
||||
$is_credit = isset($line['is_credit']) ? (int)$line['is_credit'] : 0;
|
||||
$budgetItemId = !empty($line['budget_item_id']) ? (int)$line['budget_item_id'] : null;
|
||||
|
||||
// 🔥 NOUVEAU TRAITEMENT DU BUDGET_ITEM_ID
|
||||
$rawItemId = $line['budget_item_id'] ?? '';
|
||||
$budgetItemId = null;
|
||||
$salaryId = null;
|
||||
if (!empty($rawItemId)) {
|
||||
if (strpos($rawItemId, 'SAL_') === 0) {
|
||||
$salaryId = (int)str_replace('SAL_', '', $rawItemId);
|
||||
} else {
|
||||
$budgetItemId = (int)$rawItemId;
|
||||
}
|
||||
}
|
||||
|
||||
$holidayId = !empty($line['holiday_id']) ? (int)$line['holiday_id'] : null;
|
||||
$gestionMonthLine = !empty($line['gestion_month']) ? $line['gestion_month'] . '-01' : $viewMonthDate;
|
||||
|
||||
@@ -80,8 +90,8 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
|
||||
$dateToSave = $line['date'];
|
||||
|
||||
try {
|
||||
$stmtExp->execute([$dateToSave, $gestionMonthLine, $cat, $line['label'], $finalAmount, $line['ref'], $budgetItemId, $holidayId]);
|
||||
$stmtRule->execute([$line['label'], $cat]);
|
||||
$stmtExp->execute([$dateToSave, $gestionMonthLine, $cat, $line['label'], $finalAmount, $line['ref'], $budgetItemId, $holidayId, $salaryId]);
|
||||
$stmtRule->execute([$line['label'], $cat, $budgetItemId]);
|
||||
$count++;
|
||||
} catch (Exception $e) { continue; }
|
||||
}
|
||||
@@ -110,7 +120,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,18 +147,59 @@ 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;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 3. RECUPERATION DES DONNEES ET CALCULS
|
||||
// 3. RECUPERATION DES DONNEES ET CALCULS (100% DYNAMIQUE)
|
||||
// ============================================================================
|
||||
|
||||
// --- LECTURE DYNAMIQUE DES CATEGORIES ---
|
||||
$stmtCats = $pdo->query("SELECT * FROM pf_budget_categories ORDER BY type DESC, sort_order ASC, label ASC");
|
||||
$dbCategories = $stmtCats->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$categoriesConfig = [];
|
||||
foreach ($dbCategories as $c) {
|
||||
$catType = ($c['type'] === 'Income') ? 'credit' : 'debit';
|
||||
$categoriesConfig[$c['code']] = [
|
||||
'type' => $catType,
|
||||
'db_type' => $c['type'],
|
||||
'label' => ($c['icon'] ? $c['icon'] . ' ' : '') . $c['label'],
|
||||
'budget' => 0, // Sera rempli par les règles
|
||||
'color' => $c['color'] ?: '#64748b',
|
||||
'suggestions' => []
|
||||
];
|
||||
}
|
||||
|
||||
// Fallback "Autres" au cas où la BDD serait vide ou pour les dépenses non classées
|
||||
if (!isset($categoriesConfig['AUTRES'])) {
|
||||
$categoriesConfig['AUTRES'] = [
|
||||
'type'=>'debit', 'db_type'=>'Expense', 'label'=>'📁 Autres / Divers',
|
||||
'budget'=>0, 'color'=>'#94a3b8', 'suggestions'=>[]
|
||||
];
|
||||
}
|
||||
|
||||
// Peuplement dynamique des suggestions via les règles existantes
|
||||
$stmtRules = $pdo->query("SELECT keyword, category FROM pf_import_rules");
|
||||
while ($rule = $stmtRules->fetch(PDO::FETCH_ASSOC)) {
|
||||
if (isset($categoriesConfig[$rule['category']])) {
|
||||
$categoriesConfig[$rule['category']]['suggestions'][] = $rule['keyword'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$stmtCheckClose = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'month_closure' AND reference_id = ?");
|
||||
$stmtCheckClose->execute([$viewMonthDate]);
|
||||
$closureJson = $stmtCheckClose->fetchColumn();
|
||||
@@ -170,27 +229,44 @@ $allExpenses = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$paidItemIds = array_column(array_filter($allExpenses, fn($e) => !empty($e['budget_item_id'])), 'budget_item_id');
|
||||
$realExpensesLabels = array_column(array_filter($allExpenses, fn($e) => $e['amount'] < 0), 'label');
|
||||
|
||||
$budget_fmcg = 0; $budget_school = 0; $budget_essence = 0; $budget_frais = 0; $budget_income_prevu = 0;
|
||||
$budget_income_prevu = 0;
|
||||
$total_income = 0; $total_expenses_prevues = 0;
|
||||
$reste_a_venir_calc = 0;
|
||||
$fixedChargesList = []; $incomeList = []; $pending_charges = [];
|
||||
|
||||
$stmt = $pdo->query("SELECT id, name, amount, type, category, is_estimate, payment_day, mapping_keywords FROM pf_budget_items ORDER BY name ASC");
|
||||
// ============================================================================
|
||||
// MAPPING DYNAMIQUE DES BUDGETS PRÉVISIONNELS (NOUVELLE LOGIQUE)
|
||||
// ============================================================================
|
||||
// On exclut "SAVINGS" pour s'aligner à 100% avec le recap.php
|
||||
$stmt = $pdo->query("SELECT id, name, amount, type, category, is_estimate, payment_day, mapping_keywords FROM pf_budget_items WHERE category != 'SAVINGS' ORDER BY name ASC");
|
||||
$estimatesList = []; // On prépare la liste pour retenir nos estimations multi-catégories
|
||||
|
||||
while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$absAmount = abs((float)$item['amount']);
|
||||
$amt = ($item['type'] === 'Annuel') ? $absAmount / 12 : $absAmount;
|
||||
$name = trim($item['name']);
|
||||
$catCode = $item['category'];
|
||||
|
||||
if ($item['category'] === 'expense' && $item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
|
||||
$fixedChargesList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
|
||||
}
|
||||
if ($item['category'] === 'income') {
|
||||
$isIncome = (strtoupper($catCode) === 'INCOME');
|
||||
|
||||
if ($isIncome) {
|
||||
$incomeList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
|
||||
$total_income += $amt;
|
||||
$budget_income_prevu += $amt;
|
||||
|
||||
if (!empty($catCode) && isset($categoriesConfig[$catCode])) {
|
||||
$categoriesConfig[$catCode]['budget'] += $amt;
|
||||
} else {
|
||||
$incomeCatKey = array_key_first(array_filter($categoriesConfig, fn($c) => $c['db_type'] === 'Income'));
|
||||
if ($incomeCatKey) $categoriesConfig[$incomeCatKey]['budget'] += $amt;
|
||||
}
|
||||
} else {
|
||||
$total_expenses_prevues += $amt;
|
||||
if ($item['category'] === 'expense' && $item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
|
||||
|
||||
// 1. C'est une charge fixe (Non-variable)
|
||||
if ($item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
|
||||
$fixedChargesList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
|
||||
|
||||
$isPaid = false;
|
||||
if (in_array($item['id'], $paidItemIds)) $isPaid = true;
|
||||
elseif (!empty($item['mapping_keywords'])) {
|
||||
@@ -204,83 +280,112 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
}
|
||||
if (!$isPaid) {
|
||||
$reste_a_venir_calc += $absAmount;
|
||||
$pending_charges[] = ['name' => $name, 'amount' => $absAmount];
|
||||
}
|
||||
}
|
||||
if ($name === 'Estimacio F&B & beauty') $budget_fmcg = $amt;
|
||||
elseif ($name === 'Estimacio escola') $budget_school = $amt;
|
||||
elseif ($name === 'Estimation gasolina') $budget_essence = $amt;
|
||||
elseif ((int)$item['is_estimate'] === 0 && $item['type'] === 'Mensuel' && $item['category'] === 'expense') { $budget_frais += $absAmount; }
|
||||
// Ajouté dans le tableau avec un tooltip vide
|
||||
$pending_charges[] = ['name' => $name, 'amount' => $absAmount, 'tooltip' => 'Charge fixe en attente'];
|
||||
}
|
||||
}
|
||||
|
||||
$budget_autres = max(0, $total_income - $total_expenses_prevues);
|
||||
|
||||
// TRADUCTION DES CATÉGORIES
|
||||
$categoriesConfig = [
|
||||
'Income' => ['type'=>'credit', 'label'=>tr('cat_income'), 'budget'=>$budget_income_prevu, 'color'=>'#10b981', 'suggestions'=>[]],
|
||||
'FMCG' => ['type'=>'debit', 'label'=>tr('cat_fmcg'), 'budget'=>$budget_fmcg, 'color'=>'#3b82f6', 'suggestions'=>['Action', 'Carrefour', 'Lidl']],
|
||||
'Essence' => ['type'=>'debit', 'label'=>tr('cat_fuel'), 'budget'=>$budget_essence, 'color'=>'#f59e0b', 'suggestions'=>['Audi', 'Polo']],
|
||||
'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']],
|
||||
'LivretA' => ['type'=>'debit', 'label'=>tr('cat_savings'),'budget'=>0, 'color'=>'#8b5cf6', 'suggestions'=>['Virement']]
|
||||
// 2. C'est une estimation (Variable)
|
||||
if ((int)$item['is_estimate'] === 1) {
|
||||
$estimatesList[] = [
|
||||
'name' => $name,
|
||||
'amount' => $amt,
|
||||
'categories' => !empty($catCode) ? array_map('trim', explode(',', $catCode)) : []
|
||||
];
|
||||
}
|
||||
|
||||
// 3. Attribution du budget aux jauges visuelles de la page
|
||||
// Pour ne pas tout casser, on donne tout le plafond visuel à la 1ère catégorie de la liste
|
||||
if (!empty($catCode)) {
|
||||
$catCodesArray = array_map('trim', explode(',', $catCode));
|
||||
$firstCat = $catCodesArray[0];
|
||||
if (isset($categoriesConfig[$firstCat])) {
|
||||
$categoriesConfig[$firstCat]['budget'] += $amt;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$stmtSalaries = $pdo->query("SELECT id, person, salary FROM pf_salary_config WHERE year = " . $viewY);
|
||||
while ($sal = $stmtSalaries->fetch(PDO::FETCH_ASSOC)) {
|
||||
$incomeList[] = [
|
||||
'id' => 'SAL_' . $sal['id'],
|
||||
'name' => 'Salaire ' . $sal['person'],
|
||||
'amount' => (float)$sal['salary']
|
||||
];
|
||||
}
|
||||
|
||||
// L'enveloppe "Autres" prend tout le reste du budget non alloué
|
||||
$categoriesConfig['AUTRES']['budget'] = max(0, $total_income - $total_expenses_prevues);
|
||||
|
||||
$totals = array_fill_keys(array_keys($categoriesConfig), 0);
|
||||
$expensesByCategory = array_fill_keys(array_keys($categoriesConfig), []);
|
||||
$total_rentrees = 0;
|
||||
$depenses_reelles = 0;
|
||||
|
||||
// VENTILATION DYNAMIQUE DES DÉPENSES
|
||||
foreach ($allExpenses as $exp) {
|
||||
$cat = $exp['category'];
|
||||
if (!isset($totals[$cat])) $cat = 'Autres';
|
||||
if (!isset($categoriesConfig[$cat])) $cat = 'AUTRES';
|
||||
|
||||
$val = (float)$exp['amount'];
|
||||
|
||||
if ($cat === 'Income') { $totals[$cat] += $val; }
|
||||
else {
|
||||
if ($val > 0) $categoriesConfig[$cat]['budget'] += $val;
|
||||
if ($categoriesConfig[$cat]['db_type'] === 'Income') {
|
||||
$totals[$cat] += $val;
|
||||
} else {
|
||||
if ($val > 0) $categoriesConfig[$cat]['budget'] += $val; // Remboursement
|
||||
else $totals[$cat] += abs($val);
|
||||
}
|
||||
|
||||
$expensesByCategory[$cat][] = $exp;
|
||||
|
||||
if ($val > 0) {
|
||||
if ($cat === 'Income' || $cat !== 'Frais') $total_rentrees += $val;
|
||||
else $depenses_reelles -= $val;
|
||||
if ($categoriesConfig[$cat]['db_type'] === 'Income') { $total_rentrees += $val; }
|
||||
else { $depenses_reelles -= $val; }
|
||||
} else {
|
||||
$depenses_reelles += abs($val);
|
||||
}
|
||||
}
|
||||
|
||||
// Reste à venir dynamiques
|
||||
$ecole_depense = isset($totals['School']) ? $totals['School'] : 0;
|
||||
$reste_ecole = max(0, $budget_school - $ecole_depense);
|
||||
if ($reste_ecole > 0) {
|
||||
$reste_a_venir_calc += $reste_ecole;
|
||||
$pending_charges[] = ['name' => tr('bud_rem_school'), 'amount' => $reste_ecole];
|
||||
// CALCUL DES RESTES À VENIR BASÉ SUR LES ESTIMATIONS (MACRO)
|
||||
foreach ($estimatesList as $est) {
|
||||
$spentForEstimate = 0;
|
||||
$detailsHover = [];
|
||||
|
||||
// On additionne les dépenses de toutes les catégories liées à cette estimation
|
||||
foreach ($est['categories'] as $cCode) {
|
||||
if (!empty($cCode) && isset($totals[$cCode])) {
|
||||
$spentForEstimate += $totals[$cCode];
|
||||
// On prépare le détail pour la petite bulle d'info (hover)
|
||||
if ($totals[$cCode] > 0 && isset($categoriesConfig[$cCode])) {
|
||||
$detailsHover[] = strip_tags($categoriesConfig[$cCode]['label']) . " : " . number_format($totals[$cCode], 0) . "€";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$fmcg_depense = isset($totals['FMCG']) ? $totals['FMCG'] : 0;
|
||||
$reste_fmcg = max(0, $budget_fmcg - $fmcg_depense);
|
||||
if ($reste_fmcg > 0) {
|
||||
$reste_a_venir_calc += $reste_fmcg;
|
||||
$pending_charges[] = ['name' => tr('bud_rem_fmcg'), 'amount' => $reste_fmcg];
|
||||
$rem = max(0, $est['amount'] - $spentForEstimate);
|
||||
if ($rem > 0) {
|
||||
$reste_a_venir_calc += $rem;
|
||||
$tooltip = !empty($detailsHover) ? implode(' | ', $detailsHover) : 'Aucune dépense pour le moment';
|
||||
$pending_charges[] = [
|
||||
'name' => 'Reste ' . $est['name'],
|
||||
'amount' => $rem,
|
||||
'tooltip' => $tooltip
|
||||
];
|
||||
}
|
||||
|
||||
$essence_depense = isset($totals['Essence']) ? $totals['Essence'] : 0;
|
||||
$reste_essence = max(0, $budget_essence - $essence_depense);
|
||||
if ($reste_essence > 0) {
|
||||
$reste_a_venir_calc += $reste_essence;
|
||||
$pending_charges[] = ['name' => tr('bud_rem_fuel'), 'amount' => $reste_essence];
|
||||
}
|
||||
|
||||
// F. Calculs des KPIs finaux
|
||||
$rentrees_salaires_reels = $totals['Income'] ?? 0;
|
||||
$rentrees_salaires_reels = 0;
|
||||
foreach($categoriesConfig as $code => $conf) { if($conf['db_type'] === 'Income') $rentrees_salaires_reels += ($totals[$code] ?? 0); }
|
||||
|
||||
$rentrees_autres = $total_rentrees - $rentrees_salaires_reels;
|
||||
$salaires_retenus = max($rentrees_salaires_reels, $budget_income_prevu);
|
||||
|
||||
$capacite_max_calc = $solde_initial + $salaires_retenus + $rentrees_autres;
|
||||
$solde_theorique_calc = ($solde_initial + $total_rentrees) - $depenses_reelles - $reste_a_venir_calc;
|
||||
|
||||
$revenus_a_venir = max(0, $budget_income_prevu - $rentrees_salaires_reels);
|
||||
$solde_theorique_calc = $snapshot['amount'] + $revenus_a_venir - $reste_a_venir_calc;
|
||||
|
||||
if ($isClosed) {
|
||||
$solde_actuel = $monthState['solde_actuel'];
|
||||
@@ -310,7 +415,6 @@ function getDisplayLogic($spent, $bg, $type) {
|
||||
return ['pct' => $pct, 'isOver' => $isOver, 'text' => $text];
|
||||
}
|
||||
|
||||
// Noms des mois traduits
|
||||
$monthNames = [
|
||||
1 => tr('month_01'), 2 => tr('month_02'), 3 => tr('month_03'), 4 => tr('month_04'),
|
||||
5 => tr('month_05'), 6 => tr('month_06'), 7 => tr('month_07'), 8 => tr('month_08'),
|
||||
@@ -396,7 +500,7 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
||||
<div style="text-align:center; font-style:italic; opacity:0.8;"><?= tr('bud_all_paid') ?></div>
|
||||
<?php else: ?>
|
||||
<?php foreach($pending_charges as $pc): ?>
|
||||
<div style="display:flex; justify-content:space-between; margin-bottom:6px;">
|
||||
<div style="display:flex; justify-content:space-between; margin-bottom:6px;" <?= !empty($pc['tooltip']) ? 'title="'.htmlspecialchars($pc['tooltip']).'" style="cursor:help;"' : '' ?>>
|
||||
<span><?= htmlspecialchars($pc['name']) ?></span>
|
||||
<strong><?= number_format($pc['amount'], 0, ',', ' ') ?> €</strong>
|
||||
</div>
|
||||
@@ -480,11 +584,11 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!$isClosed): ?>
|
||||
<button class="btn-add-item" style="color:<?= $conf['color'] ?>;" onclick="openAddModal('<?= $key ?>', '<?= addslashes($conf['label']) ?>')">+</button>
|
||||
<button class="btn-add-item" style="color:<?= $conf['color'] ?>;" onclick="openAddModal('<?= $key ?>', '<?= addslashes(strip_tags($conf['label'])) ?>')">+</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php $barCol = ($key === 'Income') ? '#10b981' : ($logic['isOver'] ? '#ef4444' : $conf['color']); ?>
|
||||
<?php $barCol = ($conf['db_type'] === 'Income') ? '#10b981' : ($logic['isOver'] ? '#ef4444' : $conf['color']); ?>
|
||||
<div style="background:#f1f5f9; height:4px; width:100%;">
|
||||
<div style="width:<?= $logic['pct'] ?>%; background:<?= $barCol ?>; height:100%;"></div>
|
||||
</div>
|
||||
@@ -543,7 +647,7 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
||||
<label class="pf-label"><?= tr('bud_category') ?></label>
|
||||
<select name="category" id="modalCatSelect" class="pf-input" onchange="handleModalCatChange(this)">
|
||||
<?php foreach($categoriesConfig as $key => $conf): ?>
|
||||
<option value="<?= $key ?>"><?= $conf['label'] ?></option>
|
||||
<option value="<?= $key ?>"><?= strip_tags($conf['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
@@ -562,27 +666,21 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
||||
<datalist id="modalSuggestions"></datalist>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="blockInputSelect" style="margin-bottom:15px; display:none;">
|
||||
<label class="pf-label"><?= tr('bud_beneficiary') ?></label>
|
||||
<select name="label_select" id="schoolSelect" class="pf-input">
|
||||
<option value="Ecole Pol">Ecole Pol</option>
|
||||
<option value="Carole">Carole</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="blockInputFrais" style="margin-bottom:15px; display:none;">
|
||||
<label class="pf-label"><?= tr('bud_fixed_charge') ?></label>
|
||||
<label class="pf-label">Lier à une Charge Fixe (Optionnel)</label>
|
||||
<select name="budget_item_id" id="fraisSelect" class="pf-input" disabled>
|
||||
<option value=""><?= tr('bud_select_beneficiary') ?></option>
|
||||
<option value="">-- Aucune --</option>
|
||||
<?php foreach ($fixedChargesList as $fc): ?><option value="<?= $fc['id'] ?>"><?= htmlspecialchars($fc['name']) ?></option><?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="blockInputIncome" style="margin-bottom:15px; display:none;">
|
||||
<label class="pf-label"><?= tr('bud_expected_income') ?></label>
|
||||
<label class="pf-label">Lier à un Revenu Prévu (Optionnel)</label>
|
||||
<select name="budget_item_id" id="incomeSelect" class="pf-input" disabled>
|
||||
<option value=""><?= tr('bud_select_beneficiary') ?> </option>
|
||||
<?php foreach ($incomeList as $inc): ?><option value="<?= $inc['id'] ?>"><?= htmlspecialchars($inc['name']) ?></option><?php endforeach; ?>
|
||||
<option value="">-- Aucun --</option>
|
||||
<?php foreach ($incomeList as $inc): ?>
|
||||
<option value="<?= $inc['id'] ?>"><?= htmlspecialchars($inc['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -690,16 +788,20 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
||||
<select name="lines[<?= $idx ?>][cat]" class="pf-input line-select" onchange="handleLineCatChange(this)" <?= $dis ?> style="padding:4px; font-size:0.85rem; flex:1;">
|
||||
<option value="">-- <?= $isCrd ? tr('bud_ignore') : tr('bud_to_define') ?> --</option>
|
||||
<?php foreach ($categoriesConfig as $k => $c): ?>
|
||||
<option value="<?= $k ?>" <?= ($row['cat']===$k)?'selected':'' ?>><?= $c['label'] ?></option>
|
||||
<option value="<?= $k ?>" <?= ($row['cat']===$k)?'selected':'' ?>><?= strip_tags($c['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</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; ?>
|
||||
<option value="">-- Lier à une Charge Fixe --</option>
|
||||
<?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>
|
||||
@@ -723,7 +825,6 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
||||
document.body.classList.add('no-scroll');
|
||||
<?php endif; ?>
|
||||
|
||||
// --- 1. SÉCURISATION TRADUCTIONS ET LANGUE ---
|
||||
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
||||
window.I18N = {
|
||||
...(window.I18N || {}),
|
||||
@@ -735,34 +836,46 @@ window.I18N = {
|
||||
'btn_delete': <?= json_encode(tr('btn_delete')) ?>,
|
||||
};
|
||||
|
||||
// --- DICTIONNAIRE JS DYNAMIQUE ---
|
||||
const catConfigs = <?= json_encode($categoriesConfig) ?>;
|
||||
|
||||
const activeViewMonth = '<?= substr($viewMonthDate, 0, 7) ?>';
|
||||
function toggleDiv(id) { const el = document.getElementById(id); el.style.display = (el.style.display === 'none') ? 'block' : 'none'; }
|
||||
function openSuiviModal(id) { document.getElementById(id).classList.add('open'); document.body.classList.add('no-scroll'); }
|
||||
function closeSuiviModal(id) { document.getElementById(id).classList.remove('open'); document.body.classList.remove('no-scroll');}
|
||||
|
||||
const suggestions = <?= json_encode(array_map(fn($c) => $c['suggestions'], $categoriesConfig)) ?>;
|
||||
|
||||
// Gestion dynamique du formulaire modal
|
||||
function handleModalCatChange(select) {
|
||||
const catKey = select.value;
|
||||
const catKey = select.value.toUpperCase(); // Sécurité sur la casse
|
||||
const conf = catConfigs[select.value] || {db_type: 'Expense', suggestions: []};
|
||||
|
||||
document.getElementById('blockInputText').style.display = 'none';
|
||||
document.getElementById('blockInputSelect').style.display = 'none';
|
||||
document.getElementById('blockInputText').style.display = 'block';
|
||||
document.getElementById('blockInputFrais').style.display = 'none';
|
||||
document.getElementById('blockInputIncome').style.display = 'none';
|
||||
|
||||
document.getElementById('modalLabelInput').required = false;
|
||||
document.getElementById('modalLabelInput').required = true;
|
||||
document.getElementById('fraisSelect').required = false;
|
||||
document.getElementById('incomeSelect').required = false;
|
||||
document.getElementById('fraisSelect').disabled = true;
|
||||
document.getElementById('incomeSelect').disabled = true;
|
||||
|
||||
if (catKey === 'School') { document.getElementById('blockInputSelect').style.display = 'block'; }
|
||||
else if (catKey === 'Frais') { document.getElementById('blockInputText').style.display = 'block'; document.getElementById('blockInputFrais').style.display = 'block'; document.getElementById('fraisSelect').required = true; document.getElementById('fraisSelect').disabled = false; }
|
||||
else if (catKey === 'Income') { document.getElementById('blockInputText').style.display = 'block'; document.getElementById('blockInputIncome').style.display = 'block'; document.getElementById('incomeSelect').required = true; document.getElementById('incomeSelect').disabled = false; }
|
||||
else {
|
||||
document.getElementById('blockInputText').style.display = 'block'; document.getElementById('modalLabelInput').required = true;
|
||||
if (conf.db_type === 'Income') {
|
||||
const incSel = document.getElementById('incomeSelect');
|
||||
// 👁️ Ne s'affiche que s'il y a des revenus prévus à lier, sinon reste invisible
|
||||
if (incSel.options.length > 1) {
|
||||
document.getElementById('blockInputIncome').style.display = 'block';
|
||||
incSel.disabled = false;
|
||||
}
|
||||
}
|
||||
else if (catKey === 'FIXED') { // 🔒 Strictement pour les Charges Fixes
|
||||
document.getElementById('blockInputFrais').style.display = 'block';
|
||||
document.getElementById('fraisSelect').disabled = false;
|
||||
document.getElementById('fraisSelect').required = true; // 🔥 Devient OBLIGATOIRE
|
||||
}
|
||||
|
||||
const list = document.getElementById('modalSuggestions'); list.innerHTML = '';
|
||||
if (suggestions[catKey]) { suggestions[catKey].forEach(i => { const op = document.createElement('option'); op.value = i; list.appendChild(op); }); }
|
||||
if (conf.suggestions) {
|
||||
conf.suggestions.forEach(i => { const op = document.createElement('option'); op.value = i; list.appendChild(op); });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -774,8 +887,10 @@ function openAddModal(catKey, catLabel) {
|
||||
document.getElementById('modalGestionMonth').value = activeViewMonth;
|
||||
document.getElementById('modalLabelInput').value = "";
|
||||
document.getElementById('modalAmount').value = "";
|
||||
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = catKey; handleModalCatChange(catSelect);
|
||||
document.getElementById('modalIsCredit').value = (catKey === 'Income') ? "1" : "0";
|
||||
|
||||
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = catKey;
|
||||
handleModalCatChange(catSelect);
|
||||
document.getElementById('modalIsCredit').value = (catConfigs[catKey]?.db_type === 'Income') ? "1" : "0";
|
||||
}
|
||||
|
||||
function openEditModal(e) {
|
||||
@@ -787,17 +902,35 @@ function openEditModal(e) {
|
||||
document.getElementById('modalLabelInput').value = e.label;
|
||||
document.getElementById('modalIsCredit').value = parseFloat(e.amount) > 0 ? "1" : "0";
|
||||
document.getElementById('modalAmount').value = Math.abs(parseFloat(e.amount));
|
||||
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = e.category; handleModalCatChange(catSelect);
|
||||
if (e.category === 'Frais') document.getElementById('fraisSelect').value = e.budget_item_id;
|
||||
else if (e.category === 'Income') document.getElementById('incomeSelect').value = e.budget_item_id;
|
||||
|
||||
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = e.category;
|
||||
handleModalCatChange(catSelect);
|
||||
|
||||
if (catConfigs[e.category]?.db_type === 'Income') document.getElementById('incomeSelect').value = e.budget_item_id;
|
||||
else document.getElementById('fraisSelect').value = e.budget_item_id;
|
||||
}
|
||||
|
||||
function handleLineCatChange(select) {
|
||||
// Gestion dynamique du CSV Import
|
||||
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;
|
||||
if (select.value === 'Frais') { fSel.style.display = 'block'; fSel.disabled = false; }
|
||||
else if (select.value === 'Income') { iSel.style.display = 'block'; iSel.disabled = false; }
|
||||
const fSel = row.querySelector('.select-frais');
|
||||
const iSel = row.querySelector('.select-income');
|
||||
const catKey = select.value.toUpperCase();
|
||||
const conf = catConfigs[select.value] || null;
|
||||
|
||||
fSel.style.display = 'none'; iSel.style.display = 'none';
|
||||
if (!isInit) { fSel.value = ''; iSel.value = ''; }
|
||||
fSel.disabled = true; iSel.disabled = true;
|
||||
|
||||
if (conf && conf.db_type === 'Income') {
|
||||
// 👁️ Ne s'affiche que s'il y a des revenus à lier
|
||||
if (iSel.options.length > 1) {
|
||||
iSel.style.display = 'block'; iSel.disabled = false;
|
||||
}
|
||||
}
|
||||
else if (catKey === 'FIXED') { // 🔒 Strictement pour les Charges Fixes
|
||||
fSel.style.display = 'block'; fSel.disabled = false;
|
||||
}
|
||||
checkValidation();
|
||||
}
|
||||
|
||||
@@ -806,27 +939,45 @@ function toggleAll(src) { document.querySelectorAll('.line-checkbox:not([disable
|
||||
function checkValidation() {
|
||||
let miss = 0;
|
||||
document.querySelectorAll('.line-checkbox:checked').forEach(cb => {
|
||||
const row = cb.closest('tr'); const isCrd = row.querySelector('.is-credit-flag').value === '1'; const cat = row.querySelector('.line-select').value;
|
||||
let v = true; if (cat==="") { if(!isCrd) v = false; } else if (cat==='Frais' && row.querySelector('.select-frais').value==="") v=false; else if (cat==='Income' && row.querySelector('.select-income').value==="") v=false;
|
||||
const row = cb.closest('tr');
|
||||
const isCrd = row.querySelector('.is-credit-flag').value === '1';
|
||||
const cat = row.querySelector('.line-select').value;
|
||||
const catKey = cat.toUpperCase();
|
||||
const fSel = row.querySelector('.select-frais');
|
||||
|
||||
let v = true;
|
||||
if (cat === "") {
|
||||
// La catégorie est obligatoire pour les dépenses (les revenus sont acceptés sans cat si souhaité)
|
||||
if (!isCrd) v = false;
|
||||
}
|
||||
else if (catKey === 'FIXED' && fSel.value === "") {
|
||||
v = false; // 🔥 Bloque l'import si une Charge Fixe n'est pas liée à une ligne du budget !
|
||||
}
|
||||
|
||||
if (!v) { miss++; row.style.background = '#fff1f2'; } else row.style.background = '';
|
||||
});
|
||||
|
||||
const btn = document.getElementById('btnImport'); const msg = document.getElementById('missingCount');
|
||||
if(miss>0) { btn.disabled = true; btn.style.opacity=0.5; msg.style.display='inline'; msg.innerText = miss + ' ' + (window.I18N['bud_to_define_js'] || ''); }
|
||||
if(miss > 0) { btn.disabled = true; btn.style.opacity = 0.5; msg.style.display = 'inline'; msg.innerText = miss + ' ' + (window.I18N['bud_to_define_js'] || 'à définir'); }
|
||||
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')) {
|
||||
e.target.style.display = 'none';
|
||||
e.target.classList.remove('open', 'is-active');
|
||||
e.target.style.display = '';
|
||||
document.body.classList.remove('no-scroll');
|
||||
}
|
||||
});
|
||||
|
||||
// --- 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();
|
||||
@@ -856,7 +1007,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
submitBtn.innerText = '⏳ ...';
|
||||
|
||||
const formData = new FormData(formExpense);
|
||||
// Force l'action ici pour être sûr
|
||||
formData.set('action', 'save_expense_manual');
|
||||
|
||||
const actionUrl = formExpense.getAttribute('action') || 'modules/budget/includes/api/manage-item.php';
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
/* En-tête & Boutons */
|
||||
/* =========================================================
|
||||
EN-TÊTE & BOUTONS
|
||||
========================================================= */
|
||||
.fc-header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -62,7 +64,9 @@
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
/* Calendrier Mensuel */
|
||||
/* =========================================================
|
||||
CALENDRIER MENSUEL (Vue Grille Mois)
|
||||
========================================================= */
|
||||
.fc-month-calendar-wrapper {
|
||||
background: white;
|
||||
border: 1px solid var(--pf-border);
|
||||
@@ -114,6 +118,7 @@
|
||||
|
||||
.fc-view-controls {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
width: 100% !important;
|
||||
background: #f1f5f9 !important;
|
||||
padding: 4px !important;
|
||||
@@ -128,7 +133,7 @@
|
||||
padding: 6px 16px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--pf-text-muted);
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
@@ -206,7 +211,9 @@
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
/* Décompte Congés (Pills) */
|
||||
/* =========================================================
|
||||
DÉCOMPTE CONGÉS MENSUEL (PILLS)
|
||||
========================================================= */
|
||||
.fc-month-balances {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -226,12 +233,6 @@
|
||||
border: 1px solid var(--pf-border);
|
||||
box-shadow: var(--pf-shadow-sm);
|
||||
}
|
||||
.fc-minimal-balance-card strong.alex {
|
||||
color: var(--text-alex);
|
||||
}
|
||||
.fc-minimal-balance-card strong.laia {
|
||||
color: var(--text-laia);
|
||||
}
|
||||
.fc-minimal-chips {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -249,17 +250,17 @@
|
||||
}
|
||||
.fc-min-chip .type {
|
||||
font-weight: 700;
|
||||
color: var(--pf-text-muted);
|
||||
color: #64748b;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.fc-min-chip .val {
|
||||
font-weight: 800;
|
||||
color: var(--pf-text-main);
|
||||
color: #0f172a;
|
||||
}
|
||||
.fc-used-badge {
|
||||
margin-left: 6px;
|
||||
background: #fee2e2;
|
||||
color: var(--pf-danger);
|
||||
color: #ef4444;
|
||||
padding: 1px 5px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.65rem;
|
||||
@@ -272,7 +273,7 @@
|
||||
font-size: 0.9rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--pf-danger);
|
||||
color: #ef4444;
|
||||
font-weight: bold;
|
||||
}
|
||||
@keyframes pulseBurn {
|
||||
@@ -290,7 +291,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Couleurs des jours */
|
||||
/* =========================================================
|
||||
COULEURS DES JOURS (GRILLES)
|
||||
========================================================= */
|
||||
.fc-day--school-holiday {
|
||||
background: var(--c-school-holiday) !important;
|
||||
}
|
||||
@@ -304,35 +307,13 @@
|
||||
.fc-day--extra-off-carole {
|
||||
background: var(--c-extra-off) !important;
|
||||
}
|
||||
|
||||
.fc-day--selected {
|
||||
background: var(--c-selected) !important;
|
||||
outline: 2px solid var(--pf-primary);
|
||||
z-index: 5;
|
||||
}
|
||||
.fc-day--centre::after {
|
||||
content: "🏫";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.fc-day--avis::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: url("/modules/family-calendar/assets/img/avis.svg") no-repeat
|
||||
center;
|
||||
background-size: contain;
|
||||
}
|
||||
.fc-pep-sick-emoji {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.fc-day--off-carole.fc-day--school-holiday,
|
||||
.fc-day--extra-off-carole.fc-day--school-holiday,
|
||||
.fc-day--centre.fc-day--school-holiday,
|
||||
@@ -340,7 +321,26 @@
|
||||
box-shadow: inset 0 -8px 0 0 var(--c-school-holiday) !important;
|
||||
}
|
||||
|
||||
/* Planning Hebo (Desktop) */
|
||||
.fc-day--today {
|
||||
background: #eff6ff !important;
|
||||
box-shadow: inset 0 0 0 1px #bfdbfe !important;
|
||||
}
|
||||
.fc-day--today .fc-day-number {
|
||||
display: inline;
|
||||
background: none;
|
||||
width: auto;
|
||||
height: auto;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
color: var(--pf-primary);
|
||||
font-weight: 900;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
PLANNING GLOBAL (Matrice Annuelle)
|
||||
========================================================= */
|
||||
.fc-week-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -379,22 +379,17 @@
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
HARMONISATION DES BORDURES ET HEADERS STICKY (1px strict)
|
||||
========================================================= */
|
||||
/* HARMONISATION DES BORDURES ET HEADERS STICKY */
|
||||
#planningTable thead tr {
|
||||
height: 30px; /* On fixe la hauteur pour un calcul parfait */
|
||||
height: 30px;
|
||||
}
|
||||
#planningTable thead th {
|
||||
position: sticky;
|
||||
background: #f8fafc !important;
|
||||
border: none !important; /* On supprime les bordures natives */
|
||||
|
||||
/* On utilise UNIQUEMENT des ombres internes pour simuler 1px de bordure sans doublement */
|
||||
border: none !important;
|
||||
box-shadow:
|
||||
inset 0 -1px 0 var(--pf-border),
|
||||
inset -1px 0 0 var(--pf-border) !important;
|
||||
|
||||
padding: 6px;
|
||||
font-size: 0.75rem;
|
||||
z-index: 20;
|
||||
@@ -403,14 +398,14 @@
|
||||
}
|
||||
#planningTable thead tr:nth-child(1) th {
|
||||
top: 0;
|
||||
z-index: 22; /* Au premier plan */
|
||||
z-index: 22;
|
||||
}
|
||||
#planningTable thead tr:nth-child(2) th {
|
||||
top: 30px; /* Exactement la hauteur de la ligne 1 */
|
||||
top: 30px;
|
||||
z-index: 21;
|
||||
}
|
||||
#planningTable thead tr:nth-child(3) th {
|
||||
top: 60px; /* Ligne 1 + Ligne 2 */
|
||||
top: 60px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
@@ -425,10 +420,9 @@
|
||||
color: var(--text-laia) !important;
|
||||
}
|
||||
|
||||
/* Cellules standards du corps */
|
||||
#planningTable tbody td {
|
||||
position: relative;
|
||||
border: none !important; /* Reset de sécurité */
|
||||
border: none !important;
|
||||
border-right: 1px solid var(--pf-border) !important;
|
||||
border-bottom: 1px solid var(--pf-border) !important;
|
||||
height: 36px;
|
||||
@@ -467,13 +461,36 @@ td.col-laia-sub {
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
font-size: 0.7rem;
|
||||
color: var(--pf-text-muted);
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
GESTION DES COLONNES STICKY (Mois & Semaine)
|
||||
========================================================= */
|
||||
/* ==========================================================================
|
||||
OPTIMISATION DES LARGEURS DU TABLEAU HEBDOMADAIRE
|
||||
========================================================================== */
|
||||
|
||||
/* 1. On limite les colonnes des jours (Lun-Ven) pour qu'elles prennent ~55% de la page max */
|
||||
#planningTable th.col-day,
|
||||
#planningTable td.col-day {
|
||||
width: 11%;
|
||||
max-width: 90px;
|
||||
}
|
||||
|
||||
/* 2. On compacte la colonne "Sem." qui n'affiche plus que 2 chiffres */
|
||||
#planningTable th.col-sticky-sem,
|
||||
#planningTable td.col-sticky-sem {
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
padding: 4px; /* Réduit les marges internes */
|
||||
}
|
||||
|
||||
/* 3. On s'assure que le tableau utilise tout l'espace disponible intelligemment */
|
||||
#planningTable {
|
||||
table-layout: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* GESTION DES COLONNES STICKY (Mois & Semaine) */
|
||||
#planningTable tbody td.col-sticky-mois {
|
||||
position: sticky !important;
|
||||
left: 0 !important;
|
||||
@@ -481,7 +498,6 @@ td.col-laia-sub {
|
||||
background: white !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#planningTable tbody td.col-sticky-mois .fc-sticky-mois-label {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@@ -491,42 +507,31 @@ td.col-laia-sub {
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 2. Sticky Semaine (Body) */
|
||||
#planningTable tbody td.col-sticky-sem {
|
||||
position: sticky !important;
|
||||
left: 54px !important;
|
||||
z-index: 14 !important;
|
||||
background: white !important;
|
||||
/* Ombre portée pour délimiter la zone sticky du reste du tableau */
|
||||
box-shadow: 4px 0 6px -2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 3. Sticky Mois (Header) */
|
||||
#planningTable thead tr th.col-sticky-mois {
|
||||
position: sticky !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
z-index: 30 !important;
|
||||
background: var(--pf-bg-lighter) !important;
|
||||
/* Les bordures sont gérées par le box-shadow général du thead th */
|
||||
}
|
||||
|
||||
/* 4. Sticky Semaine (Header) */
|
||||
#planningTable thead tr th.col-sticky-sem {
|
||||
position: sticky !important;
|
||||
top: 0 !important;
|
||||
left: 54px !important;
|
||||
z-index: 29 !important;
|
||||
background: var(--pf-bg-lighter) !important;
|
||||
/* Ombre interne (bordures) + Ombre portée (effet sticky) */
|
||||
box-shadow:
|
||||
inset 0 -1px 0 var(--pf-border),
|
||||
inset -1px 0 0 var(--pf-border),
|
||||
4px 0 6px -2px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
/* Rétablissement horizontal pour les headers mois et semaine */
|
||||
#planningTable thead th.col-month,
|
||||
#planningTable tbody td.col-sticky-sem {
|
||||
writing-mode: horizontal-tb;
|
||||
@@ -535,15 +540,15 @@ td.col-laia-sub {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Panneaux bas & Légendes */
|
||||
/* =========================================================
|
||||
PANNEAUX BAS, LÉGENDES & RÉSUMÉS
|
||||
========================================================= */
|
||||
.fc-bottom-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* Nouvelles bordures style "Budget" pour les conteneurs */
|
||||
.pf-card {
|
||||
background: white;
|
||||
border: 1px solid var(--pf-border);
|
||||
@@ -560,6 +565,7 @@ td.col-laia-sub {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
/* Légendes statiques */
|
||||
.pf-legend-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -607,53 +613,62 @@ td.col-laia-sub {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.fc-summary-header {
|
||||
/* Résumé Mensuel Injecté */
|
||||
.fc-month-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--pf-border);
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.fc-summ-select {
|
||||
.fc-month-summary-inline {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 5px;
|
||||
background: var(--pf-bg-lighter, #f8fafc);
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--pf-border);
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.8rem;
|
||||
height: auto;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.fc-summary-item {
|
||||
.fc-summ-pill {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 12px 4px;
|
||||
border-bottom: 1px solid var(--pf-border); /* Ligne de séparation visible */
|
||||
}
|
||||
.fc-summary-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.fc-summary-label {
|
||||
font-size: 0.85rem;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
}
|
||||
.fc-summary-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
.fc-summ-pill strong {
|
||||
font-size: 0.95rem;
|
||||
color: #0f172a;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.pf-presence-pill strong {
|
||||
color: var(--pf-primary);
|
||||
}
|
||||
|
||||
/* Menu Contextuel Tactile */
|
||||
[data-theme="dark"] .fc-month-summary-inline {
|
||||
background: #1c2128;
|
||||
border-color: #30363d;
|
||||
}
|
||||
[data-theme="dark"] .fc-summ-pill strong {
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
MENU CONTEXTUEL (COMPACT & PROPRE)
|
||||
========================================================= */
|
||||
#selectionMenu,
|
||||
#fc-month-selectionMenu,
|
||||
.fc-selection-menu {
|
||||
position: absolute;
|
||||
z-index: 9000;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
box-shadow: var(--pf-shadow-lg);
|
||||
border-radius: 16px;
|
||||
padding: 10px 12px;
|
||||
min-width: 220px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 12px;
|
||||
padding: 8px 12px;
|
||||
min-width: 210px;
|
||||
display: none;
|
||||
animation: menuPopIn 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
transform-origin: top center;
|
||||
@@ -668,88 +683,70 @@ td.col-laia-sub {
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fc-menu-section {
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px dashed #cbd5e1;
|
||||
}
|
||||
.fc-menu-section:last-child {
|
||||
border: none;
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.fc-menu-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.fc-menu-section strong {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--pf-text-muted);
|
||||
}
|
||||
.fc-menu-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
}
|
||||
.fc-menu-btn {
|
||||
background: white;
|
||||
border: 1px solid var(--pf-border);
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
transition: 0.2s;
|
||||
box-shadow: var(--pf-shadow-sm);
|
||||
text-align: left;
|
||||
transition: all 0.2s ease;
|
||||
color: #0f172a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.fc-menu-btn:hover {
|
||||
background: var(--pf-bg-page);
|
||||
border-color: var(--pf-primary);
|
||||
color: var(--pf-primary);
|
||||
background: #f1f5f9;
|
||||
}
|
||||
.fc-menu-clear-icon {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
border-radius: 6px;
|
||||
|
||||
/* --- LE BOUTON ACTIF (SANS COCHE !) --- */
|
||||
.fc-menu-btn--active {
|
||||
border: 1px solid var(--pf-primary) !important;
|
||||
background: #eff6ff !important;
|
||||
color: var(--pf-primary) !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
.fc-menu-clear-icon svg {
|
||||
/* Sécurité absolue pour tuer les coches fantômes */
|
||||
.fc-menu-btn--active::before,
|
||||
.fc-menu-btn--active::after {
|
||||
display: none !important;
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
/* Icônes intégrées dans le JS */
|
||||
.fc-icon-centre {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.fc-icon-avis {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.fc-menu-clear-icon:hover {
|
||||
background: #fef2f2;
|
||||
color: var(--pf-danger);
|
||||
}
|
||||
.fc-menu-leaves-table table {
|
||||
width: 100%;
|
||||
border-spacing: 2px;
|
||||
}
|
||||
.fc-menu-leaves-table th {
|
||||
font-size: 0.7rem;
|
||||
color: var(--pf-text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
.fc-th-inline {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
object-fit: contain;
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* --- BOUTON DE MENU ACTIF (Élément déjà enregistré) --- */
|
||||
.fc-menu-btn--active {
|
||||
background: #eff6ff;
|
||||
border-color: var(--pf-primary);
|
||||
color: var(--pf-primary);
|
||||
/* Un léger inset shadow pour rendre la bordure un peu plus épaisse sans casser la taille */
|
||||
box-shadow: inset 0 0 0 1px var(--pf-primary);
|
||||
}
|
||||
|
||||
/* Table specifique modale vacances */
|
||||
/* =========================================================
|
||||
MODALES (Paramètres & Vacances)
|
||||
========================================================= */
|
||||
.fc-holidays-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@@ -772,7 +769,6 @@ td.col-laia-sub {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
/* === MODALES : Alignement du Header et de la Croix === */
|
||||
.pf-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -781,14 +777,12 @@ td.col-laia-sub {
|
||||
border-bottom: 1px solid var(--pf-border);
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.pf-modal-header .pf-modal-title {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-bottom: none; /* On annule la bordure par défaut du global.css pour la déléguer au header */
|
||||
border-bottom: none;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.pf-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
@@ -799,9 +793,37 @@ td.col-laia-sub {
|
||||
padding: 0 0 0 15px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.pf-modal-close:hover {
|
||||
color: #ef4444; /* Devient rouge au survol */
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.fc-sidebar-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 1.2rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
color: #0f172a;
|
||||
transition: all 0.2s;
|
||||
width: calc(100% - 16px);
|
||||
margin: 2px 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.fc-sidebar-tab.active {
|
||||
background: #111827;
|
||||
color: #ffffff;
|
||||
}
|
||||
.fc-sidebar-tab:hover:not(.active) {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
[data-theme="dark"] .fc-sidebar-tab.active {
|
||||
background: var(--pf-primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
@@ -811,36 +833,25 @@ td.col-laia-sub {
|
||||
background: #ffffff;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
|
||||
font-family: inherit;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
|
||||
/* Padding standard */
|
||||
color: #0f172a;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-transform: capitalize;
|
||||
box-shadow: var(--pf-shadow-sm);
|
||||
}
|
||||
|
||||
.fc-smart-select:hover {
|
||||
border-color: #94a3b8;
|
||||
}
|
||||
|
||||
.fc-smart-select:focus {
|
||||
background: #ffffff;
|
||||
border-color: var(--primary);
|
||||
border-color: var(--pf-primary);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
|
||||
}
|
||||
|
||||
/* --- BOUTON AUJOURD'HUI & CONTROLES --- */
|
||||
.fc-view-controls {
|
||||
align-items: center; /* Pour bien centrer le nouveau bouton */
|
||||
}
|
||||
|
||||
.fc-today-button {
|
||||
background: white;
|
||||
border: 1px solid var(--pf-border);
|
||||
@@ -853,12 +864,10 @@ td.col-laia-sub {
|
||||
transition: all 0.2s;
|
||||
box-shadow: var(--pf-shadow-sm);
|
||||
}
|
||||
|
||||
.fc-today-button:hover {
|
||||
background: #eff6ff;
|
||||
border-color: var(--pf-primary);
|
||||
}
|
||||
|
||||
.fc-view-divider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
@@ -866,34 +875,14 @@ td.col-laia-sub {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
/* --- MISE EN ÉVIDENCE DU JOUR EN COURS --- */
|
||||
.fc-day--today {
|
||||
background: #eff6ff !important;
|
||||
box-shadow: inset 0 0 0 1px #bfdbfe !important;
|
||||
}
|
||||
|
||||
.fc-day--today .fc-day-number {
|
||||
display: inline;
|
||||
background: none;
|
||||
width: auto;
|
||||
height: auto;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
color: var(--pf-primary);
|
||||
font-weight: 900;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
MOBILE RESPONSIVE
|
||||
========================================================= */
|
||||
@media (max-width: 768px) {
|
||||
.fc-smart-select {
|
||||
font-size: 1rem;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 768px) {
|
||||
.pf-family-calendar .pf-container {
|
||||
padding: 0 !important;
|
||||
}
|
||||
@@ -927,6 +916,9 @@ td.col-laia-sub {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#selectionMenu,
|
||||
#fc-month-selectionMenu,
|
||||
.fc-selection-menu {
|
||||
position: fixed !important;
|
||||
top: auto !important;
|
||||
@@ -939,7 +931,8 @@ td.col-laia-sub {
|
||||
animation: slideUpSheet 0.3s forwards;
|
||||
z-index: 10000 !important;
|
||||
}
|
||||
.fc-selection-menu::before {
|
||||
#selectionMenu::before,
|
||||
#fc-month-selectionMenu::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 40px;
|
||||
@@ -955,3 +948,92 @@ td.col-laia-sub {
|
||||
gap: 20px !important;
|
||||
}
|
||||
}
|
||||
@keyframes slideUpSheet {
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
/* ==========================================================================
|
||||
AFFINAGE EXTRÊME DES COLONNES MOIS ET SEMAINE (Mise à jour)
|
||||
========================================================================== */
|
||||
|
||||
/* 1. Colonne Mois (Texte vertical) : On réduit à 35px */
|
||||
.col-month,
|
||||
#planningTable th.col-sticky-mois,
|
||||
#planningTable td.col-sticky-mois {
|
||||
width: 15px !important;
|
||||
min-width: 15px !important;
|
||||
max-width: 15px !important;
|
||||
padding: 2px !important;
|
||||
}
|
||||
|
||||
/* 2. Colonne Semaine (Numéro court) : On réduit à 30px */
|
||||
#planningTable th.col-sticky-sem,
|
||||
#planningTable td.col-sticky-sem {
|
||||
width: 10px !important;
|
||||
min-width: 10px !important;
|
||||
max-width: 10px !important;
|
||||
padding: 2px !important;
|
||||
font-size: 0.85rem !important;
|
||||
}
|
||||
|
||||
/* 3. CORRECTION CRITIQUE : Le décalage "Sticky" de la colonne Semaine */
|
||||
/* Puisque la colonne Mois fait 35px, la colonne Semaine doit démarrer à 35px (et non plus 54px) */
|
||||
#planningTable tbody td.col-sticky-sem,
|
||||
#planningTable thead tr th.col-sticky-sem {
|
||||
left: 15px !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
RÉGLAGES MILLIMÉTRÉS (FIX ALIGNEMENTS & STICKY)
|
||||
========================================================================== */
|
||||
|
||||
/* 1. On force un alignement strict pour éviter que les bordures de "Av." et "Use" ne se désalignent */
|
||||
#planningTable {
|
||||
table-layout: fixed !important;
|
||||
width: max-content !important;
|
||||
min-width: 100% !important;
|
||||
}
|
||||
|
||||
/* 2. Colonne Mois : Box-sizing inclut le padding pour un calcul mathématique exact */
|
||||
.col-month,
|
||||
#planningTable th.col-sticky-mois,
|
||||
#planningTable td.col-sticky-mois {
|
||||
box-sizing: border-box !important;
|
||||
width: 25px !important; /* 15px + marges de sécurité */
|
||||
min-width: 25px !important;
|
||||
max-width: 25px !important;
|
||||
padding: 2px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 3. Colonne Semaine : Le décalage (left) correspond EXACTEMENT à la largeur du Mois */
|
||||
#planningTable th.col-sticky-sem,
|
||||
#planningTable td.col-sticky-sem {
|
||||
box-sizing: border-box !important;
|
||||
left: 25px !important; /* Doit être rigoureusement égal au width du Mois ! */
|
||||
width: 20px !important; /* 10px + marges */
|
||||
min-width: 20px !important;
|
||||
max-width: 20px !important;
|
||||
padding: 2px !important;
|
||||
font-size: 0.8rem !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 4. Sécurité Anti-Trous pour les headers collants du haut (Empêche de voir à travers lors du scroll) */
|
||||
#planningTable thead tr {
|
||||
height: 32px !important;
|
||||
}
|
||||
#planningTable thead th {
|
||||
height: 32px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
#planningTable thead tr:nth-child(1) th {
|
||||
top: 0 !important;
|
||||
}
|
||||
#planningTable thead tr:nth-child(2) th {
|
||||
top: 32px !important;
|
||||
}
|
||||
#planningTable thead tr:nth-child(3) th {
|
||||
top: 64px !important;
|
||||
}
|
||||
|
||||
+1331
-1115
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
require __DIR__ . '/../../../../includes/auth.php';
|
||||
require __DIR__ . '/../../../../includes/db.php';
|
||||
require_login();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
$action = $_REQUEST['action'] ?? '';
|
||||
|
||||
try {
|
||||
// ─── 1. RECUPÉRATION COMPLÈTE DES PARAMÈTRES (INITIALISATION) ───
|
||||
if ($action === 'get_all') {
|
||||
$stmtFoyer = $pdo->query("SELECT zone_scolaire, care_modes FROM pf_foyer_settings LIMIT 1");
|
||||
$foyer = $stmtFoyer->fetch(PDO::FETCH_ASSOC) ?: ['zone_scolaire' => 'C', 'care_modes' => '[]'];
|
||||
|
||||
$stmtPeople = $pdo->query("SELECT id, name, role, care_modes, color FROM pf_people WHERE is_active = 1 ORDER BY role ASC, name ASC");
|
||||
$people = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$stmtLeaves = $pdo->query("SELECT person_id, leave_type, anniversary_date, method, allowance FROM pf_person_leave_meta");
|
||||
$leavesRaw = $stmtLeaves->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$leavesMap = [];
|
||||
foreach ($leavesRaw as $leave) {
|
||||
$leavesMap[$leave['person_id']][] = [
|
||||
'type' => $leave['leave_type'],
|
||||
'method' => $leave['method'] ?? 'FIXED',
|
||||
'allowance' => $leave['allowance'] ?? 0,
|
||||
'date' => $leave['anniversary_date']
|
||||
];
|
||||
}
|
||||
|
||||
$stmtSettings = $pdo->query("SELECT setting_key, setting_value FROM pf_settings WHERE module = 'calendar'");
|
||||
$calendarSettingsRaw = $stmtSettings->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
$calendarSettings = [
|
||||
'calendar_default_view' => $calendarSettingsRaw['calendar_default_view'] ?? 'month',
|
||||
'calendar_first_day' => $calendarSettingsRaw['calendar_first_day'] ?? '1',
|
||||
'calendar_working_hours' => $calendarSettingsRaw['calendar_working_hours'] ?? '08:00-19:00'
|
||||
];
|
||||
|
||||
// Catalogue des congés
|
||||
$stmtLeaveTypes = $pdo->query("SELECT code, label, default_allowance, reset_month, allow_carry_over FROM pf_leave_types ORDER BY label ASC");
|
||||
$leaveTypes = $stmtLeaveTypes->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'foyer' => [
|
||||
'zone_scolaire' => $foyer['zone_scolaire'],
|
||||
'care_modes' => json_decode($foyer['care_modes'] ?? '[]', true)
|
||||
],
|
||||
'people' => $people,
|
||||
'leaves' => $leavesMap,
|
||||
'calendar_settings' => $calendarSettings,
|
||||
'leave_types' => $leaveTypes
|
||||
]
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ─── 2. SAUVEGARDE DU FOYER (ONGLET 1) ───
|
||||
if ($action === 'save_foyer') {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
||||
|
||||
$zone = trim($_POST['zone_scolaire'] ?? 'C');
|
||||
$modesRaw = $_POST['care_modes'] ?? '[]';
|
||||
$modesArray = json_encode(array_values(array_unique(array_filter(json_decode($modesRaw, true)))));
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE pf_foyer_settings SET zone_scolaire = ?, care_modes = ? WHERE id = 1");
|
||||
$stmt->execute([$zone, $modesArray]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ─── 3. SAUVEGARDE DES MODES DE GARDE D'UN ENFANT ───
|
||||
if ($action === 'save_child_care_modes') {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
||||
|
||||
$person_id = (int)($_POST['person_id'] ?? 0);
|
||||
$care_modes = $_POST['care_modes'] ?? '[]';
|
||||
|
||||
if ($person_id > 0) {
|
||||
$stmt = $pdo->prepare("UPDATE pf_people SET care_modes = ? WHERE id = ?");
|
||||
$stmt->execute([$care_modes, $person_id]);
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => 'ID de l\'enfant manquant.']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// ─── 4. GESTION DU CATALOGUE DE CONGÉS (FOYER) ───
|
||||
if ($action === 'get_leave_types') {
|
||||
$stmt = $pdo->query("SELECT * FROM pf_leave_types ORDER BY label ASC");
|
||||
echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'save_leave_type') {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
||||
|
||||
$mode = $_POST['mode'] ?? 'add';
|
||||
$code = strtoupper(trim($_POST['code'] ?? ''));
|
||||
$label = trim($_POST['label'] ?? '');
|
||||
|
||||
if (empty($code) || empty($label)) throw new Exception("Le code et le label sont obligatoires.");
|
||||
|
||||
if ($mode === 'add') {
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_leave_types (code, label) VALUES (?, ?)");
|
||||
$stmt->execute([$code, $label]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("UPDATE pf_leave_types SET label = ? WHERE code = ?");
|
||||
$stmt->execute([$label, $code]);
|
||||
}
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete_leave_type') {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
||||
|
||||
$code = strtoupper(trim($_POST['code'] ?? ''));
|
||||
if (empty($code)) throw new Exception("Code du congé manquant.");
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// 1. On supprime d'abord ce congé chez tous les membres à qui on l'avait attribué
|
||||
$stmtMeta = $pdo->prepare("DELETE FROM pf_person_leave_meta WHERE leave_type = ?");
|
||||
$stmtMeta->execute([$code]);
|
||||
|
||||
// 2. Ensuite, on supprime le congé du catalogue global
|
||||
$stmtCat = $pdo->prepare("DELETE FROM pf_leave_types WHERE code = ?");
|
||||
$stmtCat->execute([$code]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ─── 5. GESTION DES CONGÉS INDIVIDUELS (MEMBRES) ───
|
||||
if ($action === 'get_person_leaves') {
|
||||
$personId = (int)($_GET['person_id'] ?? 0);
|
||||
$stmt = $pdo->prepare("SELECT id, leave_type, allowance, method, anniversary_date FROM pf_person_leave_meta WHERE person_id = ? ORDER BY leave_type ASC");
|
||||
$stmt->execute([$personId]);
|
||||
echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'add_person_leave') {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
||||
|
||||
$personId = (int)($_POST['person_id'] ?? 0);
|
||||
$type = strtoupper(trim($_POST['leave_type'] ?? ''));
|
||||
$allowance = (float)($_POST['allowance'] ?? 0);
|
||||
$resetMonth = (int)($_POST['reset_month'] ?? 1);
|
||||
|
||||
$method = in_array($_POST['method'] ?? '', ['FIXED', 'ACCUMULATED']) ? $_POST['method'] : 'FIXED';
|
||||
|
||||
if ($personId <= 0 || empty($type)) throw new Exception("Données invalides.");
|
||||
|
||||
// Vérification des doublons
|
||||
$check = $pdo->prepare("SELECT id FROM pf_person_leave_meta WHERE person_id = ? AND leave_type = ?");
|
||||
$check->execute([$personId, $type]);
|
||||
if ($check->rowCount() > 0) throw new Exception("Ce congé est déjà attribué à cette personne.");
|
||||
|
||||
// On construit la date anniversaire avec le mois choisi par l'utilisateur
|
||||
$anniversaryDate = "2000-" . str_pad((string)$resetMonth, 2, "0", STR_PAD_LEFT) . "-01";
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_person_leave_meta (person_id, leave_type, allowance, method, anniversary_date) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$personId, $type, $allowance, $method, $anniversaryDate]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete_person_leave') {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$stmt = $pdo->prepare("DELETE FROM pf_person_leave_meta WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
throw new Exception("Action inconnue : " . htmlspecialchars($action));
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
exit;
|
||||
}
|
||||
@@ -51,3 +51,11 @@
|
||||
[2026-05-06T20:38:24+02:00] RAW INPUT: [{"date":"2026-05-08","type":"AVIS","duration":1,"person":"Carole"}]
|
||||
[2026-05-06T20:43:43+02:00] RAW INPUT: [{"date":"2026-05-06","type":"AVIS","duration":1,"person":"Carole"}]
|
||||
[2026-05-06T20:44:36+02:00] RAW INPUT: [{"date":"2026-05-06","type":"PEP_SICK","duration":1,"person":"Carole"}]
|
||||
[2026-05-20T16:40:17+02:00] RAW INPUT: [{"date":"2026-05-19","type":"OFF_CAROLE","duration":1,"person":"Carole"}]
|
||||
[2026-06-16T14:20:35+02:00] RAW INPUT: [{"date":"2026-07-29","type":"CARE_MODE","duration":1,"person":"Centre"}]
|
||||
[2026-06-16T14:20:43+02:00] RAW INPUT: [{"date":"2026-06-16","type":"CARE_MODE","duration":1,"person":"Centre"}]
|
||||
[2026-06-16T14:22:51+02:00] RAW INPUT: [{"date":"2026-06-16","type":"CHILD_SICK","duration":1,"person":"5"}]
|
||||
[2026-06-16T14:23:10+02:00] RAW INPUT: [{"date":"2026-06-16","type":"CHILD_SICK","duration":1,"person":"5"}]
|
||||
[2026-06-16T14:23:33+02:00] RAW INPUT: [{"date":"2026-06-18","type":"CARE_MODE","duration":1,"person":"Nounou"}]
|
||||
[2026-06-16T14:23:54+02:00] RAW INPUT: [{"date":"2026-06-11","type":"HELPER_OFF","duration":1,"person":"1"}]
|
||||
[2026-06-16T14:23:57+02:00] RAW INPUT: [{"date":"2026-06-25","type":"CHILD_SICK","duration":1,"person":"4"}]
|
||||
|
||||
@@ -3,13 +3,22 @@ header('Content-Type: application/json');
|
||||
require __DIR__ . '/../../../../includes/db.php';
|
||||
|
||||
try {
|
||||
// 🔥 LE FIX : On interroge la NOUVELLE table des quotas individuels !
|
||||
$stmt = $pdo->query("
|
||||
SELECT person_id, leave_type, initial_balance, balance_year
|
||||
FROM pf_leave_balances
|
||||
SELECT
|
||||
person_id,
|
||||
leave_type AS type,
|
||||
allowance,
|
||||
method,
|
||||
anniversary_date AS date
|
||||
FROM pf_person_leave_meta
|
||||
");
|
||||
$balances = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode(['balances' => $balances]);
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'balances' => $balances
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
@@ -17,3 +26,4 @@ try {
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
?>
|
||||
@@ -8,77 +8,98 @@ $action = $input['action'] ?? '';
|
||||
|
||||
if (!$action) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Action manquante.']);
|
||||
echo json_encode(['success' => false, 'status' => 'error', 'message' => 'Action manquante.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// --- SUPPRESSION UNITAIRE ---
|
||||
if ($action === 'delete') {
|
||||
$eventId = (int)($input['event_id'] ?? 0);
|
||||
if ($eventId <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'ID manquant.']);
|
||||
echo json_encode(['success' => false, 'status' => 'error', 'message' => 'ID manquant.']);
|
||||
exit;
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM pf_events WHERE id = ?");
|
||||
$stmt->execute([$eventId]);
|
||||
echo json_encode(['status' => 'success']);
|
||||
echo json_encode(['success' => true, 'status' => 'success']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- MISE À JOUR UNITAIRE ---
|
||||
if ($action === 'update') {
|
||||
$eventId = (int)($input['event_id'] ?? 0);
|
||||
$newType = $input['new_type'] ?? '';
|
||||
if ($eventId <= 0 || !$newType) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Données manquantes.']);
|
||||
echo json_encode(['success' => false, 'status' => 'error', 'message' => 'Données manquantes.']);
|
||||
exit;
|
||||
}
|
||||
$stmt = $pdo->prepare("UPDATE pf_events SET event_type = ? WHERE id = ?");
|
||||
$stmt->execute([$newType, $eventId]);
|
||||
echo json_encode(['status' => 'success']);
|
||||
echo json_encode(['success' => true, 'status' => 'success']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'bulk_delete_day_types_person') {
|
||||
$dates = $input['dates'] ?? [];
|
||||
$types = $input['types'] ?? [];
|
||||
$person_id = $input['person_id'] ?? null;
|
||||
|
||||
$dates = array_filter($dates, function($d) { return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); });
|
||||
|
||||
if (empty($dates) || empty($types)) {
|
||||
echo json_encode(['success' => true, 'status' => 'success']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$datePlaceholders = implode(',', array_fill(0, count($dates), '?'));
|
||||
$typePlaceholders = implode(',', array_fill(0, count($types), '?'));
|
||||
|
||||
$sql = "DELETE FROM pf_events WHERE event_date IN ($datePlaceholders) AND event_type IN ($typePlaceholders)";
|
||||
$params = array_merge($dates, $types);
|
||||
|
||||
// 🔥 LE CORRECTIF : On cherche le 0 en base de données
|
||||
if ($person_id === null || $person_id === '' || (int)$person_id === 0) {
|
||||
$sql .= " AND person_id = 0";
|
||||
} else {
|
||||
$sql .= " AND person_id = ?";
|
||||
$params[] = (int)$person_id;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
|
||||
echo json_encode(['success' => true, 'status' => 'success']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- SUPPRESSION DE MASSE (Par date et type) ---
|
||||
// Utilisé quand on ajoute un événement pour nettoyer les doublons potentiels (ex: Off vs Extra)
|
||||
if ($action === 'bulk_delete_day_types') {
|
||||
$dates = $input['dates'] ?? [];
|
||||
$types = $input['types'] ?? [];
|
||||
$dates = array_filter($dates, function($d) {
|
||||
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d);
|
||||
});
|
||||
$dates = array_filter($dates, function($d) { return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); });
|
||||
|
||||
if (empty($dates) || empty($types)) {
|
||||
echo json_encode(['status' => 'success']);
|
||||
echo json_encode(['success' => true, 'status' => 'success']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Création des placeholders IN (?,?,?)
|
||||
$datePlaceholders = implode(',', array_fill(0, count($dates), '?'));
|
||||
$typePlaceholders = implode(',', array_fill(0, count($types), '?'));
|
||||
|
||||
$sql = "DELETE FROM pf_events WHERE event_date IN ($datePlaceholders) AND event_type IN ($typePlaceholders)";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
|
||||
// Fusion des tableaux pour l'exécution
|
||||
$stmt->execute(array_merge($dates, $types));
|
||||
|
||||
echo json_encode(['status' => 'success']);
|
||||
echo json_encode(['success' => true, 'status' => 'success']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- SUPPRESSION TOTALE SUR DES DATES ---
|
||||
if ($action === 'bulk_delete_all') {
|
||||
$dates = $input['dates'] ?? [];
|
||||
$dates = array_filter($dates, function($d) {
|
||||
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d);
|
||||
});
|
||||
$dates = array_filter($dates, function($d) { return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); });
|
||||
|
||||
if (empty($dates) || empty($types)) {
|
||||
echo json_encode(['status' => 'success']);
|
||||
if (empty($dates)) {
|
||||
echo json_encode(['success' => true, 'status' => 'success']);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -87,15 +108,15 @@ try {
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($dates);
|
||||
|
||||
echo json_encode(['status' => 'success']);
|
||||
echo json_encode(['success' => true, 'status' => 'success']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Action inconnue
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Action non reconnue : ' . $action]);
|
||||
echo json_encode(['success' => false, 'status' => 'error', 'message' => 'Action non reconnue : ' . $action]);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
||||
echo json_encode(['success' => false, 'status' => 'error', 'message' => $e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
@@ -1,33 +1,14 @@
|
||||
<?php
|
||||
// includes/api/save-events.php
|
||||
// modules/family-calendar/includes/api/save-events.php
|
||||
header('Content-Type: application/json');
|
||||
require __DIR__ . '/../../../../includes/db.php';
|
||||
require_once __DIR__ . '/../../../../includes/db.php';
|
||||
|
||||
try {
|
||||
$rawInput = file_get_contents('php://input');
|
||||
$eventsToSave = json_decode($rawInput, true);
|
||||
|
||||
// Optionnel : tu peux commenter/supprimer ces logs en production pour économiser du disque
|
||||
file_put_contents(
|
||||
__DIR__ . '/events-debug.log',
|
||||
"[" . date('c') . "] RAW INPUT: " . $rawInput . PHP_EOL,
|
||||
FILE_APPEND
|
||||
);
|
||||
|
||||
if (empty($eventsToSave) || !is_array($eventsToSave)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Aucune donnée d\'événement reçue.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$inserted = [];
|
||||
|
||||
try {
|
||||
// 1. OPTIMISATION : On récupère toutes les personnes d'un coup (Mapping)
|
||||
$stmtPeople = $pdo->query("SELECT id, name FROM pf_people");
|
||||
$peopleMap = [];
|
||||
while ($row = $stmtPeople->fetch(PDO::FETCH_ASSOC)) {
|
||||
// On crée un tableau associatif : ['Carole' => 1, 'Alex' => 2, etc.]
|
||||
$peopleMap[$row['name']] = $row['id'];
|
||||
throw new Exception("Aucune donnée d'événement reçue.");
|
||||
}
|
||||
|
||||
$pdo->beginTransaction();
|
||||
@@ -36,26 +17,36 @@ try {
|
||||
VALUES (:event_date, :event_type, :person_id, :duration)";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
|
||||
foreach ($eventsToSave as $event) {
|
||||
$person_id = null;
|
||||
$inserted = [];
|
||||
|
||||
// 2. On vérifie simplement dans notre tableau (plus de requête SQL ici !)
|
||||
if (!empty($event['person']) && isset($peopleMap[$event['person']])) {
|
||||
$person_id = $peopleMap[$event['person']];
|
||||
foreach ($eventsToSave as $event) {
|
||||
if (empty($event['date']) || empty($event['type'])) {
|
||||
continue; // Sécurité anti-crash
|
||||
}
|
||||
|
||||
$person_id = 0;
|
||||
if (!empty($event['person_id']) && is_numeric($event['person_id'])) {
|
||||
$person_id = (int)$event['person_id'];
|
||||
} elseif (!empty($event['person']) && is_numeric($event['person'])) {
|
||||
$person_id = (int)$event['person'];
|
||||
}
|
||||
|
||||
// 🔥 LE FIX : On tronque à 50 caractères pour éviter l'erreur "Data truncated"
|
||||
$safeType = substr(trim($event['type']), 0, 50);
|
||||
$duration = isset($event['duration']) ? (float)$event['duration'] : 1.0;
|
||||
|
||||
$stmt->execute([
|
||||
':event_date' => $event['date'],
|
||||
':event_type' => $event['type'],
|
||||
':event_type' => $safeType,
|
||||
':person_id' => $person_id,
|
||||
':duration' => $event['duration'] ?? 1.0,
|
||||
':duration' => $duration,
|
||||
]);
|
||||
|
||||
$inserted[] = [
|
||||
'id' => $pdo->lastInsertId(),
|
||||
'date' => $event['date'],
|
||||
'type' => $event['type'],
|
||||
'duration' => $event['duration'] ?? 1.0,
|
||||
'type' => $safeType,
|
||||
'duration' => $duration,
|
||||
'person_id' => $person_id,
|
||||
];
|
||||
}
|
||||
@@ -63,12 +54,20 @@ try {
|
||||
$pdo->commit();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'status' => 'success',
|
||||
'inserted' => $inserted,
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
} catch (\Throwable $e) { // 🔥 LE FIX : \Throwable attrape AUSSI les fatals errors PDO
|
||||
if (isset($pdo) && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Erreur lors de la sauvegarde : ' . $e->getMessage()]);
|
||||
}
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur Serveur/SQL : ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
?>
|
||||
+64
-7
@@ -108,15 +108,38 @@ if ($action === 'vehicles') {
|
||||
gOk($stmt->fetchAll());
|
||||
}
|
||||
if ($method === 'POST') {
|
||||
$photo = handleUpload('photo', $UPLOAD_DIR); $d = $_POST ?: gBody();
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_vehicles (name,brand,model,year,license_plate,vin,fuel_type,color,purchase_date,purchase_price,current_km,photo,notes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)");
|
||||
$stmt->execute([$d['name']??'', $d['brand']??'', $d['model']??'', $d['year']??null, $d['license_plate']??null, $d['vin']??null, $d['fuel_type']??'Essence', $d['color']??null, $d['purchase_date']??null, $d['purchase_price']??null, $d['current_km']??0, $photo, $d['notes']??null]);
|
||||
$photo = handleUpload('photo', $UPLOAD_DIR);
|
||||
$d = $_POST ?: gBody();
|
||||
|
||||
// 🔥 LE FIX EST ICI : Fonctions boucliers pour MySQL Strict Mode
|
||||
$nullInt = fn($v) => ($v === '' || $v === null) ? null : (int)$v;
|
||||
$nullFloat = fn($v) => ($v === '' || $v === null) ? null : (float)$v;
|
||||
$nullStr = fn($v) => ($v === '' || $v === null) ? null : $v;
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_vehicles (name,brand,model,year,license_plate,vin,fuel_type,consumption,color,purchase_date,purchase_price,current_km,photo,notes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
|
||||
$stmt->execute([
|
||||
$d['name'] ?? '',
|
||||
$d['brand'] ?? '',
|
||||
$d['model'] ?? '',
|
||||
$nullInt($d['year'] ?? null),
|
||||
$nullStr($d['license_plate'] ?? null),
|
||||
$nullStr($d['vin'] ?? null),
|
||||
$d['fuel_type'] ?? 'Essence',
|
||||
$nullFloat($d['consumption'] ?? null),
|
||||
$nullStr($d['color'] ?? null),
|
||||
$nullStr($d['purchase_date'] ?? null),
|
||||
$nullFloat($d['purchase_price'] ?? null),
|
||||
$nullInt($d['current_km'] ?? null) ?? 0,
|
||||
$photo,
|
||||
$nullStr($d['notes'] ?? null)
|
||||
]);
|
||||
gOk(['id' => $pdo->lastInsertId()]);
|
||||
}
|
||||
if ($method === 'PUT') {
|
||||
$id = $_GET['id'] ?? null; if (!$id) gErr('ID manquant'); $d = gBody();
|
||||
$int_fields = ['year','current_km']; $float_fields = ['purchase_price'];
|
||||
$fields = ['name','brand','model','year','license_plate','vin','fuel_type','color','purchase_date','purchase_price','current_km','notes'];
|
||||
$int_fields = ['year','current_km'];
|
||||
$float_fields = ['purchase_price', 'consumption'];
|
||||
$fields = ['name','brand','model','year','license_plate','vin','fuel_type','consumption','color','purchase_date','purchase_price','current_km','notes'];
|
||||
$sets = array_map(fn($f) => "$f = ?", $fields);
|
||||
$vals = array_map(function($f) use ($d, $int_fields, $float_fields) {
|
||||
$v = $d[$f] ?? null;
|
||||
@@ -259,7 +282,23 @@ if ($action === 'maintenances') {
|
||||
$photo = handleUpload('invoice_photo', $UPLOAD_DIR); $d = $_POST ?: gBody();
|
||||
if (!($d['vehicle_id'] ?? null)) gErr('vehicle_id manquant');
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_maintenances (vehicle_id,type,description,date,km,cost,mechanic,garage_name,next_km,next_date,invoice_photo,notes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)");
|
||||
$stmt->execute([$d['vehicle_id'], $d['type']??'', $d['description']??null, $d['date']??date('Y-m-d'), $d['km']??null, $d['cost']??0, $d['mechanic']??null, $d['garage_name']??null, $d['next_km']??null, $d['next_date']??null, $photo, $d['notes']??null]);
|
||||
$nullInt = fn($v) => ($v === '' || $v === null) ? null : (int)$v;
|
||||
$nullFloat = fn($v) => ($v === '' || $v === null) ? null : (float)$v;
|
||||
$nullStr = fn($v) => ($v === '' || $v === null) ? null : $v;
|
||||
$stmt->execute([
|
||||
$d['vehicle_id'],
|
||||
$d['type'] ?? '',
|
||||
$nullStr($d['description'] ?? null),
|
||||
$d['date'] ?? date('Y-m-d'),
|
||||
$nullInt($d['km'] ?? null),
|
||||
$nullFloat($d['cost'] ?? null),
|
||||
$nullStr($d['mechanic'] ?? null),
|
||||
$nullStr($d['garage_name'] ?? null),
|
||||
$nullInt($d['next_km'] ?? null),
|
||||
$nullStr($d['next_date'] ?? null),
|
||||
$photo,
|
||||
$nullStr($d['notes'] ?? null),
|
||||
]);
|
||||
$mid = $pdo->lastInsertId();
|
||||
if (!empty($d['km'])) { $pdo->prepare("UPDATE pf_vehicles SET current_km = GREATEST(current_km, ?), updated_at = NOW() WHERE id = ?")->execute([$d['km'], $d['vehicle_id']]); }
|
||||
gOk(['id' => $mid]);
|
||||
@@ -307,8 +346,26 @@ if ($action === 'parts') {
|
||||
}
|
||||
if ($method === 'POST') {
|
||||
$photo = handleUpload('photo', $UPLOAD_DIR); $d = $_POST ?: gBody();
|
||||
$nullInt = fn($v) => ($v === '' || $v === null) ? null : (int)$v;
|
||||
$nullFloat = fn($v) => ($v === '' || $v === null) ? null : (float)$v;
|
||||
$nullStr = fn($v) => ($v === '' || $v === null) ? null : $v;
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_parts (vehicle_id,maintenance_id,brand,reference,name,category,price,quantity,unit,supplier,purchase_date,photo,notes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)");
|
||||
$stmt->execute([$d['vehicle_id']??null, $d['maintenance_id']??null, $d['brand']??null, $d['reference']??null, $d['name']??'', $d['category']??'Autre', $d['price']??0, $d['quantity']??1, $d['unit']??'pièce', $d['supplier']??null, $d['purchase_date']??null, $photo, $d['notes']??null]);
|
||||
$stmt->execute([
|
||||
$nullInt($d['vehicle_id'] ?? null),
|
||||
$nullInt($d['maintenance_id'] ?? null),
|
||||
$nullStr($d['brand'] ?? null),
|
||||
$nullStr($d['reference'] ?? null),
|
||||
$d['name'] ?? '',
|
||||
$d['category'] ?? 'Autre',
|
||||
$nullFloat($d['price'] ?? null) ?? 0,
|
||||
$nullInt($d['quantity'] ?? null) ?? 1,
|
||||
$d['unit'] ?? 'pièce',
|
||||
$nullStr($d['supplier'] ?? null),
|
||||
$nullStr($d['purchase_date'] ?? null),
|
||||
$photo,
|
||||
$nullStr($d['notes'] ?? null)
|
||||
]);
|
||||
gOk(['id' => $pdo->lastInsertId()]);
|
||||
}
|
||||
if ($method === 'PUT') {
|
||||
|
||||
+1268
-502
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
require __DIR__ . '/../../includes/auth.php';
|
||||
require __DIR__ . '/../../includes/db.php';
|
||||
require_login();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = $_POST['action'] ?? $_GET['action'] ?? '';
|
||||
|
||||
try {
|
||||
if ($action === 'get_all') {
|
||||
$occ = $pdo->query("SELECT * FROM pf_gift_occasions ORDER BY sort_order ASC, id ASC")->fetchAll();
|
||||
$proches = $pdo->query("SELECT * FROM pf_people WHERE role IN ('proche_adulte', 'proche_enfant') ORDER BY name ASC")->fetchAll();
|
||||
echo json_encode(['success' => true, 'data' => ['occasions' => $occ, 'proches' => $proches]]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'save_occasion') {
|
||||
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
|
||||
$name = trim($_POST['name']);
|
||||
$icon = trim($_POST['icon']) ?: '🎁';
|
||||
$month_date = trim($_POST['month_date']) ?: null;
|
||||
|
||||
if (empty($name)) throw new Exception("Le nom est obligatoire.");
|
||||
|
||||
if ($id) {
|
||||
$stmt = $pdo->prepare("UPDATE pf_gift_occasions SET name=?, icon=?, month_date=? WHERE id=?");
|
||||
$stmt->execute([$name, $icon, $month_date, $id]);
|
||||
} else {
|
||||
$code = 'CUST_' . strtoupper(substr(preg_replace('/[^A-Za-z0-9]/', '', $name), 0, 4)) . '_' . rand(100, 999);
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_gift_occasions (code, name, icon, month_date) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$code, $name, $icon, $month_date]);
|
||||
}
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete_occasion') {
|
||||
$id = (int)$_POST['id'];
|
||||
$pdo->prepare("DELETE FROM pf_gift_occasions WHERE id=?")->execute([$id]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'toggle_occasion') {
|
||||
$id = (int)$_POST['id'];
|
||||
$state = (int)$_POST['state'];
|
||||
$pdo->prepare("UPDATE pf_gift_occasions SET is_active=? WHERE id=?")->execute([$state, $id]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'save_proche') {
|
||||
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
|
||||
$name = trim($_POST['name']);
|
||||
$role = $_POST['role'];
|
||||
|
||||
if (empty($name)) throw new Exception("Le nom est obligatoire.");
|
||||
if (!in_array($role, ['proche_adulte', 'proche_enfant'])) throw new Exception("Rôle invalide.");
|
||||
|
||||
if ($id) {
|
||||
// Sécurité : on s'assure qu'on modifie bien un proche et pas un parent
|
||||
$stmt = $pdo->prepare("UPDATE pf_people SET name=?, role=? WHERE id=? AND role IN ('proche_adulte', 'proche_enfant')");
|
||||
$stmt->execute([$name, $role, $id]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_people (name, role, color) VALUES (?, ?, '#94a3b8')");
|
||||
$stmt->execute([$name, $role]);
|
||||
}
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete_proche') {
|
||||
$id = (int)$_POST['id'];
|
||||
$stmt = $pdo->prepare("DELETE FROM pf_people WHERE id=? AND role IN ('proche_adulte', 'proche_enfant')");
|
||||
$stmt->execute([$id]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
exit;
|
||||
}
|
||||
+243
-91
@@ -6,7 +6,6 @@
|
||||
margin: 0;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.cl-titlebar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -17,7 +16,6 @@
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.cl-view-switch {
|
||||
display: flex;
|
||||
padding: 4px;
|
||||
@@ -25,7 +23,6 @@
|
||||
border: 1px solid var(--border-light);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.cl-view-btn {
|
||||
padding: 6px 16px;
|
||||
border-radius: 6px;
|
||||
@@ -35,13 +32,12 @@
|
||||
color: var(--text-muted);
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.cl-view-btn.is-active {
|
||||
background: var(--text-main);
|
||||
color: var(--bg-panel);
|
||||
}
|
||||
|
||||
/* === FILTRES STICKY === */
|
||||
/* === FILTRES === */
|
||||
.pf-filter-bar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
@@ -52,7 +48,6 @@
|
||||
z-index: 50;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pf-filter-label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
@@ -62,36 +57,6 @@
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pf-filter-select {
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px; /* Forme de pillule */
|
||||
border: 1px solid var(--border-light);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(8px);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--shadow-sm);
|
||||
width: auto; /* Empêche de prendre toute la largeur */
|
||||
appearance: none; /* Nettoie la flèche système... */
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
/* ...pour mettre une flèche personnalisée plus discrète */
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2364748b'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
background-size: 12px;
|
||||
padding-right: 28px; /* Place pour la flèche */
|
||||
}
|
||||
|
||||
.pf-filter-select:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
/* === SECTIONS ENFANTS === */
|
||||
.pf-child-section {
|
||||
margin-bottom: 30px;
|
||||
@@ -121,10 +86,10 @@
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
scrollbar-width: none; /* Firefox */
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.pf-child-totals-bar::-webkit-scrollbar {
|
||||
display: none; /* Chrome/Safari */
|
||||
display: none;
|
||||
}
|
||||
.pf-summary-pill {
|
||||
white-space: nowrap;
|
||||
@@ -182,12 +147,6 @@
|
||||
color: var(--success);
|
||||
font-size: 1rem;
|
||||
}
|
||||
.pf-gift-badges-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.pf-pill-adult {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
@@ -196,14 +155,6 @@
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.pf-pill-occ {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
background: #fef3c7;
|
||||
color: #b45309;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.pf-gift-payer {
|
||||
font-size: 0.7rem;
|
||||
color: #ef4444;
|
||||
@@ -241,53 +192,76 @@
|
||||
z-index: 2;
|
||||
border-right: 2px solid var(--border-light);
|
||||
}
|
||||
|
||||
/* Icônes des fêtes (Tió, Noël, etc.) plus petites et bien alignées */
|
||||
.cl-occasion-icon {
|
||||
width: 20px; /* Réduit pour être discret */
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
.gift-tricount-section {
|
||||
border: 1px solid var(--border-light);
|
||||
margin-top: 40px;
|
||||
}
|
||||
.gift-tricount-title {
|
||||
margin-top: 0;
|
||||
color: var(--text-main);
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
.gift-tricount-success {
|
||||
color: var(--success);
|
||||
font-weight: 700;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.gift-tricount-debt-amount {
|
||||
color: var(--danger);
|
||||
}
|
||||
.gift-matrix-details {
|
||||
margin-top: 20px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
.gift-matrix-summary {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
outline: none;
|
||||
}
|
||||
.gift-matrix-wrapper {
|
||||
overflow-x: auto;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.cl-debt-matrix th.sticky-col.bg-header {
|
||||
background: var(--bg-page);
|
||||
}
|
||||
.cl-debt-matrix th.sticky-col.bg-body {
|
||||
background: var(--bg-panel);
|
||||
}
|
||||
.gift-cell-danger {
|
||||
color: var(--danger);
|
||||
font-weight: 700;
|
||||
background: var(--bg-danger-soft);
|
||||
}
|
||||
.gift-cell-muted {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* === TYPOGRAPHIE & ICONES === */
|
||||
.cl-occasion-icon {
|
||||
font-size: 1.4rem;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.cl-occasion-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px; /* Espace réduit entre l'icône et le texte */
|
||||
}
|
||||
.gift-empty-state {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
font-style: italic;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pf-child-section {
|
||||
padding: 15px;
|
||||
}
|
||||
.pf-filter-bar {
|
||||
padding: 0 5px;
|
||||
}
|
||||
.pf-filter-label {
|
||||
display: none;
|
||||
}
|
||||
.pf-gift-feed {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.pf-gift-title {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.pf-gift-price {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
@media (max-width: 400px) {
|
||||
.pf-gift-feed {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
/* === COMPOSANT MULTI-SELECT VANILLA === */
|
||||
.pf-multi-select {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Le bouton déclencheur (réutilise le style des filtres) */
|
||||
.pf-ms-trigger {
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px;
|
||||
@@ -310,8 +284,6 @@
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
/* Le menu déroulant */
|
||||
.pf-ms-dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
@@ -333,8 +305,6 @@
|
||||
display: flex;
|
||||
animation: popDown 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
|
||||
/* Les options (checkboxes) */
|
||||
.pf-ms-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -375,3 +345,185 @@
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pf-child-section {
|
||||
padding: 15px;
|
||||
}
|
||||
.pf-filter-bar {
|
||||
padding: 0 5px;
|
||||
}
|
||||
.pf-filter-label {
|
||||
display: none;
|
||||
}
|
||||
.pf-gift-feed {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.pf-gift-title {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.pf-gift-price {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
@media (max-width: 400px) {
|
||||
.pf-gift-feed {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* === MODALE EDITION CADEAU === */
|
||||
.gift-modal-custom-content {
|
||||
max-width: 500px;
|
||||
width: 95%;
|
||||
}
|
||||
.gift-modal-custom-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.gift-modal-custom-title {
|
||||
margin: 0;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
.gift-modal-close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.8rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
line-height: 1;
|
||||
}
|
||||
.gift-form-group-spaced {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.gift-modal-custom-footer {
|
||||
padding-top: 15px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.gl-pane {
|
||||
display: none;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
.gl-pane.active {
|
||||
display: block;
|
||||
}
|
||||
.gl-pane-title {
|
||||
margin-top: 0;
|
||||
color: var(--text-main);
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.gl-pane-desc {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.gl-list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 14px;
|
||||
background: var(--bg-panel);
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border-light);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.gl-form-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.gl-divider {
|
||||
margin: 1.5rem 0;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
.gl-input-sm {
|
||||
flex: 0 0 60px;
|
||||
}
|
||||
.gl-input-md {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.gift-loader {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
.align-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* =========================================
|
||||
COMPOSANTS MODALE & EDITION IN-PLACE
|
||||
========================================= */
|
||||
|
||||
/* Sélecteur d'émojis horizontal */
|
||||
.emoji-picker {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.emoji-option {
|
||||
cursor: pointer;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 8px;
|
||||
font-size: 1.4rem;
|
||||
transition: all 0.2s;
|
||||
background: transparent;
|
||||
user-select: none;
|
||||
}
|
||||
.emoji-option:hover {
|
||||
background: var(--bg-page);
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
.emoji-option.selected {
|
||||
border-color: var(--primary);
|
||||
background: var(
|
||||
--bg-subtle
|
||||
); /* ou un bleu très clair rgba(59, 130, 246, 0.1) */
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
/* Alignement des boutons d'ajout */
|
||||
.modal-actions-right {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
/* Édition In-Place sur les lignes de la liste */
|
||||
.inline-edit-form {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
background: var(--bg-subtle);
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
flex-wrap: wrap; /* Évite que ça dépasse sur mobile */
|
||||
}
|
||||
.inline-edit-form .pf-input {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
+1033
-878
File diff suppressed because it is too large
Load Diff
+1624
-312
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
// modules/holidays/includes/api/delete_attachment.php
|
||||
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||
require_login();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['success' => false, 'error' => 'Méthode non autorisée']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$file_id = isset($_POST['file_id']) ? (int)$_POST['file_id'] : 0;
|
||||
$holiday_id = isset($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : 0;
|
||||
|
||||
if ($file_id === 0 || $holiday_id === 0) {
|
||||
echo json_encode(['success' => false, 'error' => 'Paramètres manquants']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 1. On récupère le chemin exact du fichier pour le supprimer du NAS
|
||||
$stmt = $pdo->prepare("SELECT file_path FROM pf_holidays_attachments WHERE id = ? AND holiday_id = ?");
|
||||
$stmt->execute([$file_id, $holiday_id]);
|
||||
$file = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($file) {
|
||||
$absolutePath = dirname(__DIR__, 4) . '/' . $file['file_path'];
|
||||
|
||||
// 2. On supprime physiquement le fichier s'il existe
|
||||
if (file_exists($absolutePath)) {
|
||||
unlink($absolutePath);
|
||||
}
|
||||
|
||||
// 3. On nettoie la base de données
|
||||
$del = $pdo->prepare("DELETE FROM pf_holidays_attachments WHERE id = ?");
|
||||
$del->execute([$file_id]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => 'Fichier introuvable ou non autorisé']);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
// modules/holidays/includes/api/get_attachments.php
|
||||
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||
require_login();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$holiday_id = isset($_GET['holiday_id']) ? (int)$_GET['holiday_id'] : 0;
|
||||
$item_id = isset($_GET['item_id']) ? (int)$_GET['item_id'] : 0;
|
||||
|
||||
if ($holiday_id === 0) {
|
||||
echo json_encode(['success' => false, 'error' => 'ID manquant']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// On récupère les documents de cette étape spécifique
|
||||
$stmt = $pdo->prepare("SELECT id, file_name, file_path FROM pf_holidays_attachments WHERE holiday_id = ? AND item_id = ? ORDER BY uploaded_at DESC");
|
||||
$stmt->execute([$holiday_id, $item_id]);
|
||||
$files = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode(['success' => true, 'files' => $files]);
|
||||
@@ -4,32 +4,117 @@ require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||
require_login();
|
||||
|
||||
// INTERCEPTION AJAX : Sauvegarde du planning (Drag & Drop / Durée)
|
||||
if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime', 'update_item_duration'])) {
|
||||
$itemId = (int)$_POST['item_id'];
|
||||
// ---------------------------------------------------------------------------
|
||||
// INTERCEPTIONS AJAX (Exécution ultra rapide sans rechargement lourd)
|
||||
// ---------------------------------------------------------------------------
|
||||
if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime', 'update_item_duration', 'add_single_item', 'update_holiday_note'])) {
|
||||
header('Content-Type: application/json');
|
||||
session_write_close();
|
||||
|
||||
try {
|
||||
if ($_POST['action'] === 'update_holiday_note') {
|
||||
$hId = (int)$_POST['holiday_id'];
|
||||
$notes = $_POST['notes'] ?? '';
|
||||
$stmt = $pdo->prepare("UPDATE pf_holidays SET notes = ? WHERE id = ?");
|
||||
$stmt->execute([$notes, $hId]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_POST['action'] === 'add_single_item') {
|
||||
$holiday_id = (int)$_POST['holiday_id'];
|
||||
$sort_order = (int)$_POST['sort_order'];
|
||||
|
||||
$stmt = $pdo->prepare("SELECT location_name, lat, lng, step_start_date, step_end_date, step_type FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ? LIMIT 1");
|
||||
$stmt->execute([$holiday_id, $sort_order]);
|
||||
$stepInfo = $stmt->fetch();
|
||||
|
||||
if ($stepInfo) {
|
||||
$dur = isset($_POST['duration']) ? (int)$_POST['duration'] : 1;
|
||||
$date = !empty($_POST['item_date']) ? $_POST['item_date'] : null;
|
||||
$time = !empty($_POST['item_time']) ? $_POST['item_time'] : null;
|
||||
|
||||
// Récupération sécurisée du contexte
|
||||
$context = !empty($_POST['expense_context']) ? $_POST['expense_context'] : (!empty($_POST['context']) ? $_POST['context'] : 'local');
|
||||
$amount = (float)$_POST['amount'];
|
||||
$name = $_POST['name'] ?? 'Trajet';
|
||||
$category = $_POST['category'] ?? 'transport';
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// 🛡️ ANTI-DOUBLON ABSOLU
|
||||
if ($context === 'transit') {
|
||||
$stmtCheck = $pdo->prepare("SELECT id FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ? AND expense_context = 'transit'");
|
||||
$stmtCheck->execute([$holiday_id, $sort_order]);
|
||||
$existingId = $stmtCheck->fetchColumn();
|
||||
|
||||
if ($existingId) {
|
||||
// Mise à jour de la ligne existante
|
||||
$stmtUp = $pdo->prepare("UPDATE pf_holidays_items SET item_date = ?, item_time = ?, amount = ?, duration = ?, name = ? WHERE id = ?");
|
||||
$stmtUp->execute([$date, $time, $amount, $dur, $name, $existingId]);
|
||||
$pdo->commit();
|
||||
echo json_encode(['success' => true, 'id' => $existingId]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Insertion si aucun doublon n'existe
|
||||
$ins = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid, location_name, lat, lng, sort_order, step_start_date, step_end_date, step_type, expense_context, duration, item_date, item_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$ins->execute([
|
||||
$holiday_id, $category, $name, $amount, 0,
|
||||
$stepInfo['location_name'], $stepInfo['lat'], $stepInfo['lng'],
|
||||
$sort_order, $stepInfo['step_start_date'], $stepInfo['step_end_date'],
|
||||
$stepInfo['step_type'], $context, $dur, $date, $time
|
||||
]);
|
||||
|
||||
$newId = $pdo->lastInsertId();
|
||||
$pdo->commit();
|
||||
|
||||
echo json_encode(['success' => true, 'id' => $newId]);
|
||||
exit;
|
||||
}
|
||||
echo json_encode(['success' => false, 'error' => 'Etape introuvable']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Drag & Drop planning : Date et Heure
|
||||
if ($_POST['action'] === 'update_item_datetime') {
|
||||
$itemId = (int)$_POST['item_id'];
|
||||
$itemDate = !empty($_POST['item_date']) ? $_POST['item_date'] : null;
|
||||
$itemTime = !empty($_POST['item_time']) ? $_POST['item_time'] : null;
|
||||
$stmt = $pdo->prepare("UPDATE pf_holidays_items SET item_date = ?, item_time = ? WHERE id = ?");
|
||||
$stmt->execute([$itemDate, $itemTime, $itemId]);
|
||||
} else {
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Changement de durée (+/-)
|
||||
if ($_POST['action'] === 'update_item_duration') {
|
||||
$itemId = (int)$_POST['item_id'];
|
||||
$duration = (int)$_POST['duration'];
|
||||
$stmt = $pdo->prepare("UPDATE pf_holidays_items SET duration = ? WHERE id = ?");
|
||||
$stmt->execute([$duration, $itemId]);
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit; // Crucial : on arrête le script ici !
|
||||
exit;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
if ($pdo->inTransaction()) { $pdo->rollBack(); }
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------===========================================
|
||||
// FORMULAIRE TRADITIONNEL (Soumission de la modale d'étape)
|
||||
// --------------------------------===========================================
|
||||
$holiday_id = (int)$_POST['holiday_id'];
|
||||
$location_name = trim($_POST['location_name']);
|
||||
$lat = (float)$_POST['lat'];
|
||||
$lng = (float)$_POST['lng'];
|
||||
$old_sort_order = isset($_POST['old_sort_order']) && $_POST['old_sort_order'] !== '' ? (int)$_POST['old_sort_order'] : null;
|
||||
|
||||
// 1. SUPPRESSION
|
||||
// SUPPRESSION DE L'ÉTAPE
|
||||
if (isset($_POST['action_delete']) && $_POST['action_delete'] === '1') {
|
||||
$pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ?")->execute([$holiday_id, $old_sort_order]);
|
||||
header("Location: /holidays.php?tab=holiday_detail&id=" . $holiday_id);
|
||||
@@ -40,26 +125,34 @@ if ($holiday_id > 0 && !empty($location_name)) {
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// 2. DÉTERMINER L'ORDRE (Identifiant de l'étape)
|
||||
if ($old_sort_order !== null) {
|
||||
// Modification : on supprime l'ancien contenu de cette étape précise
|
||||
$pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ?")->execute([$holiday_id, $old_sort_order]);
|
||||
$target_order = $old_sort_order;
|
||||
} else {
|
||||
// Ajout : on place à la fin (Max + 1)
|
||||
// NOUVEAU : Logique d'intercalage d'étape
|
||||
$insert_after = $_POST['insert_after'] ?? 'end';
|
||||
|
||||
if ($insert_after === 'end') {
|
||||
$stmtMax = $pdo->prepare("SELECT MAX(sort_order) FROM pf_holidays_items WHERE holiday_id = ?");
|
||||
$stmtMax->execute([$holiday_id]);
|
||||
$max = $stmtMax->fetchColumn();
|
||||
$target_order = ($max !== null) ? (int)$max + 1 : 0;
|
||||
} else {
|
||||
$target_order = (int)$insert_after + 1;
|
||||
// On décale toutes les étapes suivantes vers le bas
|
||||
$pdo->prepare("UPDATE pf_holidays_items SET sort_order = sort_order + 1 WHERE holiday_id = ? AND sort_order >= ?")->execute([$holiday_id, $target_order]);
|
||||
}
|
||||
}
|
||||
|
||||
// Récupération des dates de l'étape globale
|
||||
$step_start = !empty($_POST['step_start_date']) ? $_POST['step_start_date'] : null;
|
||||
$step_end = !empty($_POST['step_end_date']) ? $_POST['step_end_date'] : null;
|
||||
$is_return = isset($_POST['is_return']) ? 1 : 0; // NOUVEAU
|
||||
$step_type = $_POST['step_type'] ?? 'stop';
|
||||
|
||||
// 3. INSERTION DES LIGNES (16 Colonnes)
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid, location_name, lat, lng, sort_order, notes, item_date, item_time, step_start_date, step_end_date, duration, is_return) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
if ($step_type === 'origin' || $step_type === 'destination') {
|
||||
$step_end = null;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid, location_name, lat, lng, sort_order, notes, item_date, item_time, step_start_date, step_end_date, duration, step_type, expense_context) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$validItemsCount = 0;
|
||||
|
||||
if (isset($_POST['items']['name'])) {
|
||||
@@ -75,19 +168,26 @@ if ($holiday_id > 0 && !empty($location_name)) {
|
||||
$date = !empty($_POST['items']['date'][$i]) ? $_POST['items']['date'][$i] : null;
|
||||
$time = !empty($_POST['items']['time'][$i]) ? $_POST['items']['time'][$i] : null;
|
||||
$dur = !empty($_POST['items']['duration'][$i]) ? (int)$_POST['items']['duration'][$i] : 1;
|
||||
$context = !empty($_POST['items']['context'][$i]) ? $_POST['items']['context'][$i] : 'local';
|
||||
|
||||
// Ajout de $is_return à la fin
|
||||
$stmt->execute([$holiday_id, $cat, $name ?: tr('hdl_default_exp_name'), $amount, $paid, $location_name, $lat, $lng, $target_order, $note, $date, $time, $step_start, $step_end, $dur, $is_return]);
|
||||
// Récupération des coordonnées spécifiques à l'activité (ou fallback sur l'étape)
|
||||
$itemLat = !empty($_POST['items']['lat'][$i]) ? (float)$_POST['items']['lat'][$i] : $lat;
|
||||
$itemLng = !empty($_POST['items']['lng'][$i]) ? (float)$_POST['items']['lng'][$i] : $lng;
|
||||
|
||||
$stmt->execute([$holiday_id, $cat, $name, $amount, $paid, $location_name, $itemLat, $itemLng, $target_order, $note, $date, $time, $step_start, $step_end, $dur, $step_type, $context]);
|
||||
$validItemsCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($validItemsCount === 0) {
|
||||
$stmt->execute([$holiday_id, 'activity', 'PF_TECHNICAL_POINT', 0, 1, $location_name, $lat, $lng, $target_order, '', null, null, $step_start, $step_end, 1, $is_return]);
|
||||
$stmt->execute([$holiday_id, 'activity', 'PF_TECHNICAL_POINT', 0, 1, $location_name, $lat, $lng, $target_order, '', null, null, $step_start, $step_end, 1, $step_type, 'local']);
|
||||
}
|
||||
|
||||
if (isset($_POST['set_as_return']) && $_POST['set_as_return'] == '1') {
|
||||
$pdo->prepare("UPDATE pf_holidays SET return_step_id = ? WHERE id = ?")->execute([$target_order, $holiday_id]);
|
||||
}
|
||||
|
||||
// 4. GESTION DES FAVORIS
|
||||
if (isset($_POST['save_favorite']) && $_POST['save_favorite'] == '1') {
|
||||
$stmtFav = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'holiday_favorites'");
|
||||
$favs = json_decode($stmtFav->fetchColumn() ?: '[]', true);
|
||||
@@ -100,7 +200,10 @@ if ($holiday_id > 0 && !empty($location_name)) {
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
} catch (Exception $e) { $pdo->rollBack(); die($e->getMessage()); }
|
||||
} catch (Exception $e) {
|
||||
$pdo->rollBack();
|
||||
die($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
header("Location: /holidays.php?tab=holiday_detail&id=" . $holiday_id);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
// modules/holidays/includes/api/save_holiday.php
|
||||
|
||||
// On remonte de 4 niveaux pour atteindre la racine (api -> includes -> holidays -> modules -> racine)
|
||||
// On remonte de 4 niveaux pour atteindre la racine
|
||||
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||
require_login(); // Si cette fonction nécessite une redirection, gère-la dans auth.php
|
||||
require_login();
|
||||
|
||||
if (isset($_POST['action_delete']) && $_POST['action_delete'] == '1') {
|
||||
$stmt = $pdo->prepare("DELETE FROM pf_holidays WHERE id = ?");
|
||||
@@ -22,61 +22,60 @@ $status = $_POST['status'];
|
||||
$food = !empty($_POST['budget_food']) ? $_POST['budget_food'] : 0;
|
||||
$extra = !empty($_POST['budget_extra']) ? $_POST['budget_extra'] : 0;
|
||||
$notes = $_POST['notes'];
|
||||
// 🔥 NOUVEAU : On récupère le véhicule optionnel
|
||||
$vehicle_id = !empty($_POST['vehicle_id']) ? (int)$_POST['vehicle_id'] : null;
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// 1. Détection d'un changement de nom pour refaire l'appel API
|
||||
$image_url = null;
|
||||
$old_title = null;
|
||||
if ($id) {
|
||||
$stmtOld = $pdo->prepare("SELECT title, image_url FROM pf_holidays WHERE id = ?");
|
||||
$stmtOld->execute([$id]);
|
||||
if ($row = $stmtOld->fetch()) {
|
||||
$old_title = $row['title'];
|
||||
$image_url = $row['image_url'];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Appel à Pixabay si nouveau voyage ou si le titre a changé
|
||||
if (!$id || strcasecmp($old_title ?? '', $title) !== 0) {
|
||||
$apiKey = '56931941-a86fbea4e14b88712cc1e9ed9';
|
||||
$query = urlencode($title);
|
||||
// On force des photos horizontales HD
|
||||
$url = "https://pixabay.com/api/?key={$apiKey}&q={$query}&image_type=photo&orientation=horizontal&min_width=1280&per_page=3";
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 3); // Timeout de 3s max pour ne pas bloquer l'UI
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($response) {
|
||||
$data = json_decode($response, true);
|
||||
if (!empty($data['hits'][0]['largeImageURL'])) {
|
||||
$image_url = $data['hits'][0]['largeImageURL'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Sauvegarde en Base
|
||||
if ($id) {
|
||||
// UPDATE
|
||||
$sql = "UPDATE pf_holidays SET title=?, period_hint=?, start_date=?, end_date=?, status=?, budget_food=?, budget_extra=?, notes=? WHERE id=?";
|
||||
$sql = "UPDATE pf_holidays SET title=?, period_hint=?, start_date=?, end_date=?, status=?, budget_food=?, budget_extra=?, notes=?, vehicle_id=?, image_url=? WHERE id=?";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes, $id]);
|
||||
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes, $vehicle_id, $image_url, $id]);
|
||||
} else {
|
||||
// INSERT
|
||||
$sql = "INSERT INTO pf_holidays (title, period_hint, start_date, end_date, status, budget_food, budget_extra, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
$sql = "INSERT INTO pf_holidays (title, period_hint, start_date, end_date, status, budget_food, budget_extra, notes, vehicle_id, image_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes]);
|
||||
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes, $vehicle_id, $image_url]);
|
||||
$id = $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
// GESTION INTELLIGENTE DES ITEMS
|
||||
if (!empty($_POST['items']['name'])) {
|
||||
$count = count($_POST['items']['name']);
|
||||
// On prépare une requête qui met à jour si l'ID existe, sinon insère
|
||||
$stmtItem = $pdo->prepare("
|
||||
INSERT INTO pf_holidays_items (id, holiday_id, category, name, amount, is_paid, location_name)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
category = VALUES(category),
|
||||
name = VALUES(name),
|
||||
amount = VALUES(amount),
|
||||
is_paid = VALUES(is_paid)
|
||||
");
|
||||
|
||||
$keepIds = [];
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$itemId = !empty($_POST['items']['id'][$i]) ? (int)$_POST['items']['id'][$i] : null;
|
||||
$cat = $_POST['items']['cat'][$i] ?? 'activity';
|
||||
$name = trim($_POST['items']['name'][$i] ?? '');
|
||||
$amount = floatval($_POST['items']['amount'][$i] ?? 0);
|
||||
$paid = (int)($_POST['items']['paid'][$i] ?? 0);
|
||||
$loc = !empty($_POST['items']['location'][$i]) ? $_POST['items']['location'][$i] : null;
|
||||
|
||||
if (!empty($name)) {
|
||||
$stmtItem->execute([$itemId, $id, $cat, $name, $amount, $paid, $loc]);
|
||||
$keepIds[] = $itemId ?: $pdo->lastInsertId();
|
||||
}
|
||||
}
|
||||
|
||||
// Nettoyage : On supprime les items qui ont été retirés de la modale
|
||||
// (Attention : uniquement ceux du voyage actuel qui ne sont plus dans la liste envoyée)
|
||||
if (!empty($keepIds)) {
|
||||
$placeholders = implode(',', array_fill(0, count($keepIds), '?'));
|
||||
$sqlDel = "DELETE FROM pf_holidays_items WHERE holiday_id = ? AND id NOT IN ($placeholders)";
|
||||
$pdo->prepare($sqlDel)->execute(array_merge([$id], $keepIds));
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
} catch (Exception $e) {
|
||||
@@ -84,6 +83,5 @@ try {
|
||||
die("Erreur base de données : " . $e->getMessage());
|
||||
}
|
||||
|
||||
// Redirection vers la page principale
|
||||
header("Location: /holidays.php");
|
||||
exit;
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
// modules/holidays/includes/api/upload_attachment.php
|
||||
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||
require_login();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['success' => false, 'error' => 'Méthode non autorisée']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$holiday_id = isset($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : 0;
|
||||
$item_id = (isset($_POST['item_id']) && (int)$_POST['item_id'] > 0) ? (int)$_POST['item_id'] : null;
|
||||
|
||||
if ($holiday_id === 0 || empty($_FILES['file'])) {
|
||||
echo json_encode(['success' => false, 'error' => 'Données manquantes ou fichier invalide']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$file = $_FILES['file'];
|
||||
|
||||
// On range les fichiers dans un sous-dossier par voyage pour rester propre sur le NAS
|
||||
$uploadDir = dirname(__DIR__, 4) . '/uploads/holidays/' . $holiday_id . '/';
|
||||
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0777, true);
|
||||
}
|
||||
|
||||
// Sécurisation du nom de fichier (retrait des accents et caractères spéciaux)
|
||||
$fileName = basename($file['name']);
|
||||
$safeFileName = preg_replace("/[^a-zA-Z0-9.-]/", "_", $fileName);
|
||||
$uniqueFileName = time() . '_' . $safeFileName;
|
||||
$destination = $uploadDir . $uniqueFileName;
|
||||
|
||||
if (move_uploaded_file($file['tmp_name'], $destination)) {
|
||||
// Enregistrement en base de données
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_holidays_attachments (holiday_id, item_id, file_name, file_path) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$holiday_id, $item_id, $fileName, 'uploads/holidays/' . $holiday_id . '/' . $uniqueFileName]);
|
||||
|
||||
$attachmentId = $pdo->lastInsertId();
|
||||
echo json_encode(['success' => true, 'id' => $attachmentId, 'file_name' => $fileName]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => 'Erreur lors de l\'écriture du fichier sur le NAS']);
|
||||
}
|
||||
+570
-184
@@ -11,11 +11,18 @@ if ($id === 0) {
|
||||
// Récupération des données du voyage
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT h.*,
|
||||
v.name as vehicle_name,
|
||||
v.consumption as vehicle_consumption,
|
||||
(COALESCE(h.budget_food, 0) + COALESCE(h.budget_extra, 0) + COALESCE((SELECT SUM(amount) FROM pf_holidays_items WHERE holiday_id = h.id), 0)) as total_cost,
|
||||
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND is_paid = 1) as total_paid,
|
||||
(SELECT COALESCE(SUM(amount), 0) FROM pf_savings WHERE holiday_id = h.id) as total_saved
|
||||
FROM pf_holidays h WHERE h.id = ?
|
||||
(SELECT COALESCE(SUM(amount), 0) FROM pf_savings WHERE holiday_id = h.id) as total_saved,
|
||||
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND (expense_context = 'transit' OR (category = 'transport' AND (name LIKE '%Essence%' OR name LIKE '%Carburant%')))) as total_fuel,
|
||||
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND category = 'transport' AND (name LIKE '%Péage%' OR name LIKE '%Peage%')) as total_tolls
|
||||
FROM pf_holidays h
|
||||
LEFT JOIN pf_vehicles v ON h.vehicle_id = v.id
|
||||
WHERE h.id = ?
|
||||
");
|
||||
|
||||
$stmt->execute([$id]);
|
||||
$holiday = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
@@ -42,7 +49,7 @@ foreach ($items as $it) {
|
||||
'sort_order' => $it['sort_order'],
|
||||
'step_start_date' => $it['step_start_date'],
|
||||
'step_end_date' => $it['step_end_date'],
|
||||
'is_return' => (int)$it['is_return'],
|
||||
'step_type' => $it['step_type'] ?? 'stop',
|
||||
'total_amount' => 0,
|
||||
'items' => []
|
||||
];
|
||||
@@ -59,8 +66,9 @@ $mapPoints = array_values($steps);
|
||||
// Affichage de la date
|
||||
$dateDisplay = htmlspecialchars($holiday['period_hint'] ?? '');
|
||||
if (empty($dateDisplay) && $holiday['start_date']) {
|
||||
$dateDisplay = date('d/m/Y', strtotime($holiday['start_date']));
|
||||
if ($holiday['end_date']) $dateDisplay .= ' → ' . date('d/m/Y', strtotime($holiday['end_date']));
|
||||
// 💡 Format Jour/Mois uniquement
|
||||
$dateDisplay = date('d/m', strtotime($holiday['start_date']));
|
||||
if ($holiday['end_date']) $dateDisplay .= ' → ' . date('d/m', strtotime($holiday['end_date']));
|
||||
}
|
||||
|
||||
$cost = (float)$holiday['total_cost'];
|
||||
@@ -69,166 +77,196 @@ $saved = (float)$holiday['total_saved'];
|
||||
$leftToPay = max(0, $cost - $paid);
|
||||
$pctPaid = $cost > 0 ? min(100, ($paid / $cost) * 100) : 0;
|
||||
$pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||
|
||||
// Image de fond (API Pixabay ou fallback par défaut)
|
||||
$bgImage = !empty($holiday['image_url']) ? htmlspecialchars($holiday['image_url']) : '/modules/home/assets/img/background.jpg';
|
||||
?>
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
|
||||
<style>
|
||||
.mobile-only { display: none !important; }
|
||||
@media (max-width: 768px) {
|
||||
.desktop-only { display: none !important; }
|
||||
.mobile-only { display: flex !important; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="pf-holidays-detail">
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 20px; flex-wrap: wrap; gap: 15px;">
|
||||
<div style="display: flex; flex-direction: column; gap: 12px;">
|
||||
<a href="?tab=list" class="pf-btn btn-secondary pf-btn-small" style="width: fit-content; text-decoration: none;"><?= tr('btn_back') ?></a>
|
||||
<div style="display: flex; align-items: center; gap: 15px;">
|
||||
<h1 style="margin: 0; font-size: 1.8rem;"><?= htmlspecialchars($holiday['title']) ?></h1>
|
||||
<span class="hol-badge-status"><?= tr('hdl_status_' . $holiday['status']) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- Scripts JSON invisibles -->
|
||||
<script id="holidayDataJson" type="application/json">
|
||||
<?= json_encode(['main' => $holiday, 'items' => $generalItems], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>
|
||||
</script>
|
||||
<button type="button" class="pf-btn btn-secondary pf-btn-small" onclick="editHoliday(JSON.parse(document.getElementById('holidayDataJson').textContent))">
|
||||
<?= tr('btn_edit_bases') ?>
|
||||
|
||||
<!-- 1. HERO HEADER IMMERSIF -->
|
||||
<div class="hol-hero-banner">
|
||||
<img src="<?= $bgImage ?>" class="hol-hero-bg-img" alt="">
|
||||
<div class="hol-hero-overlay"></div>
|
||||
<div class="hol-hero-content">
|
||||
|
||||
<div class="hol-hero-top">
|
||||
<a href="?tab=list" class="hol-hero-back-link">◀ <?= tr('btn_back') ?></a>
|
||||
<span class="hol-hero-status-badge"><?= tr('hdl_status_' . $holiday['status']) ?></span>
|
||||
</div>
|
||||
|
||||
<h1 class="hol-hero-title"><?= htmlspecialchars($holiday['title']) ?></h1>
|
||||
<div class="hol-hero-date-badge">
|
||||
🗓️ <?= $dateDisplay ?>
|
||||
</div>
|
||||
|
||||
<!-- LES PILLS (Indicateurs Route & Quotidien) -->
|
||||
<div class="hol-hero-pills">
|
||||
|
||||
<!-- PILL ROUTE (Masqué par défaut, géré par OSRM) -->
|
||||
<span id="block_total_distance" class="hol-pill-block hol-hidden">
|
||||
<span class="hol-pill">
|
||||
🛣️ <span id="global_total_distance">0</span> km (⏱️ <span id="global_total_duration">0h00</span>)
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!-- PILL PÉAGES -->
|
||||
<span class="hol-pill">
|
||||
💳 <?= number_format($holiday['total_tolls'], 0) ?> €
|
||||
</span>
|
||||
|
||||
<!-- PILL ESSENCE (Avec Edit + Oeil) -->
|
||||
<span class="hol-pill">
|
||||
⛽ <span id="global_fuel_cost"><?= number_format($holiday['total_fuel'], 0) ?></span> €
|
||||
<span onclick="updateFuelPrice()" class="hol-pill-action" title="Modifier le prix de l'essence">(<span id="display_fuel_price">1.85</span>€/L) ✏️</span>
|
||||
<?php if ($holiday['total_fuel'] > 0 || $holiday['total_tolls'] > 0): ?>
|
||||
<span onclick="openTransitModal()" class="hol-pill-action" title="Voir le détail des trajets">👁️</span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
|
||||
<!-- PILL BURGER (Food) -->
|
||||
<span class="hol-pill">
|
||||
🍔 <?= number_format($holiday['budget_food'], 0) ?> €
|
||||
</span>
|
||||
|
||||
<!-- PILL CADEAU (Extras) -->
|
||||
<span class="hol-pill">
|
||||
🎁 <?= number_format($holiday['budget_extra'], 0) ?> €
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- FINANCES GLOBALES -->
|
||||
<div class="hol-hero-finances">
|
||||
<div class="hol-finance-block">
|
||||
<div class="hol-finance-label"><?= tr('hdl_total_budget') ?></div>
|
||||
<div class="hol-finance-value"><?= number_format($cost, 0, ',', ' ') ?> €</div>
|
||||
</div>
|
||||
<div class="hol-finance-block hol-finance-right">
|
||||
<div class="hol-finance-label"><?= tr('hdl_left_to_pay') ?></div>
|
||||
<div class="hol-finance-value hol-finance-value-due"><?= number_format($leftToPay, 0, ',', ' ') ?> €</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Barre de progression -->
|
||||
<div class="hol-progress-bar">
|
||||
<div class="hol-progress-paid" style="width: <?= $pctPaid ?>%;"></div>
|
||||
<div class="hol-progress-saved" style="width: <?= $pctSaved ?>%;"></div>
|
||||
</div>
|
||||
<div class="hol-hero-progress-labels">
|
||||
<span class="hol-label-paid-text">✓ <?= tr('hdl_paid') ?> : <?= number_format($paid, 0, ',', ' ') ?> €</span>
|
||||
<span class="hol-label-saved-text">💼 <?= tr('hdl_saved') ?> : <?= number_format($saved, 0, ',', ' ') ?> €</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. BARRE D'ACTIONS RAPIDES -->
|
||||
<div class="hol-quick-actions-bar">
|
||||
<button type="button" class="pf-btn btn-secondary pf-btn-small hol-quick-action-btn" onclick="toggleTripMap()">
|
||||
🗺️ Carte
|
||||
</button>
|
||||
<button type="button" class="pf-btn btn-secondary pf-btn-small hol-quick-action-btn" onclick="openGlobalPlanningModal()">
|
||||
📅 Planning
|
||||
</button>
|
||||
<button type="button" class="pf-btn btn-secondary pf-btn-small hol-quick-action-btn" onclick="generateTravelBook()">
|
||||
📖 PDF
|
||||
</button>
|
||||
<button type="button" class="pf-btn btn-secondary pf-btn-small hol-quick-action-btn" onclick="editHoliday(JSON.parse(document.getElementById('holidayDataJson').textContent))">
|
||||
⚙️ Modifier
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hol-summary-card">
|
||||
<div class="hol-summary-grid">
|
||||
<div class="hol-summary-item">
|
||||
<div class="hol-summary-label"><?= tr('hdl_label_period') ?></div>
|
||||
<div class="hol-summary-value"><?= $dateDisplay ?: tr('hdl_dates_to_define') ?></div>
|
||||
</div>
|
||||
<div class="hol-summary-item">
|
||||
<div class="hol-summary-label"><?= tr('hdl_label_budget_food_extras') ?></div>
|
||||
<div class="hol-summary-value">🍔 <?= number_format($holiday['budget_food'], 0) ?> € | 🎁 <?= number_format($holiday['budget_extra'], 0) ?> €</div>
|
||||
</div>
|
||||
<div class="hol-summary-item">
|
||||
<div class="hol-summary-label"><?= tr('hdl_total_budget') ?></div>
|
||||
<div class="hol-summary-value total"><?= number_format($cost, 0, ',', ' ') ?> €</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hol-progress-bar">
|
||||
<div class="hol-progress-paid" style="width:<?= $pctPaid ?>%;" title="<?= tr('hdl_paid') ?>"></div>
|
||||
<div class="hol-progress-saved" style="width:<?= $pctSaved ?>%;" title="<?= tr('hdl_saved') ?>"></div>
|
||||
</div>
|
||||
<div class="hol-progress-labels">
|
||||
<span class="hol-label-paid">✓ <?= tr('hdl_paid') ?> : <?= number_format($paid, 0, ',', ' ') ?> €</span>
|
||||
<span class="hol-label-saved">💼 <?= tr('hdl_saved') ?> : <?= number_format($saved, 0, ',', ' ') ?> €</span>
|
||||
<span class="hol-label-left">⏳ <?= tr('hdl_left_to_pay') ?> : <?= number_format($leftToPay, 0, ',', ' ') ?> €</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hol-layout-grid">
|
||||
<div class="hol-panel">
|
||||
<div class="hol-panel-header" style="flex-wrap: wrap; gap: 10px;">
|
||||
<div class="hol-panel-header-group">
|
||||
<h3 style="margin:0;"><?= tr('hdl_map_itinerary') ?></h3>
|
||||
<!-- 3. LA CARTE (Masquée par défaut) -->
|
||||
<div id="tripMapWrapper" class="hol-map-wrapper hol-hidden">
|
||||
<div class="hol-panel-header hol-map-panel-header">
|
||||
<h3 class="hol-map-title">Itinéraire</h3>
|
||||
<div class="hol-map-legend">
|
||||
<span class="hol-legend-item" title="<?= tr('hdl_outbound') ?>"><span class="hol-legend-color aller"></span> <?= tr('hdl_outbound') ?></span>
|
||||
<span class="hol-legend-item" title="<?= tr('hdl_return') ?>"><span class="hol-legend-color retour"></span> <?= tr('hdl_return') ?></span>
|
||||
<span class="hol-legend-item"><span class="hol-legend-color aller"></span> Aller</span>
|
||||
<span class="hol-legend-item"><span class="hol-legend-color retour"></span> Retour</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="pf-btn pf-btn-small" onclick="openCheckpointModal('add')"><?= tr('hdl_btn_add_step') ?></button>
|
||||
<!-- IMPORTANT : On garde l'ID tripMap pour que Leaflet (OSRM) fonctionne ! -->
|
||||
<div id="tripMap" class="hol-map-canvas"></div>
|
||||
</div>
|
||||
<div id="tripMap" style="flex:1; width:100%; min-height: 400px; background:#f1f5f9; position: relative; z-index: 1;"></div> </div>
|
||||
|
||||
<div class="hol-panel">
|
||||
<div class="hol-panel-header">
|
||||
<h3 style="margin:0;"><?= tr('hdl_step_details') ?></h3>
|
||||
<!-- 4. TIMELINE (Carnet de route Pleine Largeur) -->
|
||||
<div class="hol-panel hol-panel-timeline">
|
||||
<div class="hol-panel-header hol-timeline-header">
|
||||
<h3><?= tr('hdl_step_details') ?></h3>
|
||||
<button class="pf-btn pf-btn-small" onclick="openCheckpointModal('add')">+ <?= tr('hdl_btn_add_step') ?></button>
|
||||
</div>
|
||||
|
||||
<div class="hol-panel-body">
|
||||
<?php if (empty($steps)): ?>
|
||||
<p style="color:var(--text-muted); font-style:italic; text-align:center; margin-top:40px;"><?= tr('hdl_no_steps') ?></p>
|
||||
<p class="hol-empty-steps-text"><?= tr('hdl_no_steps') ?></p>
|
||||
<?php else: ?>
|
||||
|
||||
<?php if (!empty($generalItems) || $holiday['budget_food'] > 0 || $holiday['budget_extra'] > 0): ?>
|
||||
<div class="hol-checkpoint" style="border-left-color: #64748b; background: #f8fafc; margin-bottom: 20px;">
|
||||
<div class="hol-cp-header" style="background: transparent;">
|
||||
<div class="hol-cp-info-group">
|
||||
<span style="font-size:1.4rem;">🌍</span>
|
||||
<div class="hol-cp-title"><?= tr('hdl_general_costs') ?></div>
|
||||
</div>
|
||||
<?php
|
||||
$generalTotal = $holiday['budget_food'] + $holiday['budget_extra'];
|
||||
foreach ($generalItems as $gi) { $generalTotal += $gi['amount']; }
|
||||
?>
|
||||
<div class="hol-cp-actions-group">
|
||||
<div class="hol-cp-total"><?= number_format($generalTotal, 2, ',', ' ') ?> €</div>
|
||||
<button onclick='editHoliday(JSON.parse(document.getElementById("holidayDataJson").textContent))' class="btn-icon-small" title="<?= tr('btn_edit') ?>">⚙️</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hol-cp-body">
|
||||
<?php if ($holiday['budget_food'] > 0): ?>
|
||||
<div class="hol-expense-wrapper"><div class="hol-expense-main">
|
||||
<span class="hol-expense-name" style="color:#64748b;">🍔 <?= tr('hdl_food_bev') ?></span>
|
||||
<span><strong class="hol-expense-amount"><?= number_format($holiday['budget_food'], 2, ',', ' ') ?> €</strong><span class="status-pending">⏳</span></span>
|
||||
</div></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($holiday['budget_extra'] > 0): ?>
|
||||
<div class="hol-expense-wrapper"><div class="hol-expense-main">
|
||||
<span class="hol-expense-name" style="color:#64748b;">🎁 <?= tr('hdl_extras') ?></span>
|
||||
<span><strong class="hol-expense-amount"><?= number_format($holiday['budget_extra'], 2, ',', ' ') ?> €</strong><span class="status-pending">⏳</span></span>
|
||||
</div></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($generalItems as $it):
|
||||
$icon = match($it['category']) { 'transport' => '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' };
|
||||
?>
|
||||
<div class="hol-expense-wrapper"><div class="hol-expense-main">
|
||||
<span class="hol-expense-name"><?= $icon ?> <?= htmlspecialchars($it['name']) ?></span>
|
||||
<span>
|
||||
<strong class="hol-expense-amount"><?= number_format($it['amount'], 2, ',', ' ') ?> €</strong>
|
||||
<span class="<?= $it['is_paid'] ? 'status-paid' : 'status-pending' ?>"><?= $it['is_paid'] ? '✓' : '⏳' ?></span>
|
||||
</span>
|
||||
</div></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($steps as $step): ?>
|
||||
<div id="step-card-<?= $step['sort_order'] ?>" class="hol-checkpoint hol-checkpoint-draggable" draggable="true" data-location="<?= htmlspecialchars($step['location_name']) ?>">
|
||||
<div class="hol-cp-header">
|
||||
<div class="hol-cp-info-group">
|
||||
<span class="desktop-only" style="color:#94a3b8; font-size:1.1rem; cursor:grab; user-select:none;">☰</span>
|
||||
<div class="mobile-only" style="flex-direction:column; gap:4px; margin-right:8px;">
|
||||
<button type="button" onclick="moveStepMobile(this, -1)" class="btn-icon-small" style="width:24px!important; height:24px!important; font-size:0.6rem;">▲</button>
|
||||
<button type="button" onclick="moveStepMobile(this, 1)" class="btn-icon-small" style="width:24px!important; height:24px!important; font-size:0.6rem;">▼</button>
|
||||
<div class="hol-step-header">
|
||||
|
||||
<div class="hol-step-row-top">
|
||||
<?php
|
||||
$stepIcon = '📍';
|
||||
if ($step['step_type'] === 'origin') {
|
||||
$stepIcon = '🛫';
|
||||
} elseif ($step['step_type'] === 'destination') {
|
||||
$stepIcon = '🛬';
|
||||
}
|
||||
$isReturn = ($holiday['return_step_id'] !== null && $holiday['return_step_id'] == $step['sort_order']);
|
||||
?>
|
||||
<div class="hol-step-icon hol-step-icon-<?= htmlspecialchars($step['step_type']) ?>">
|
||||
<?= $stepIcon ?>
|
||||
</div>
|
||||
<div class="hol-cp-title" onclick="panMapTo(<?= $step['lat'] ?>, <?= $step['lng'] ?>)" title="<?= htmlspecialchars($step['location_name']) ?>">
|
||||
📍 <?= htmlspecialchars($step['location_name']) ?>
|
||||
<?php if (!empty($step['is_return'])): ?>
|
||||
<span style="background: #fff7ed; color: #ea580c; padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: bold; margin-left: 5px; border: 1px solid #ffedd5; vertical-align: middle;">🏁 <?= tr('hdl_return') ?></span>
|
||||
|
||||
<div class="hol-step-title" onclick="panMapTo(<?= $step['lat'] ?>, <?= $step['lng'] ?>)" title="<?= htmlspecialchars($step['location_name']) ?>">
|
||||
<?= htmlspecialchars($step['location_name']) ?>
|
||||
</div>
|
||||
|
||||
<div class="hol-step-controls">
|
||||
<span class="desktop-only hol-drag-handle">⣿</span>
|
||||
<div class="mobile-only hol-step-arrows">
|
||||
<button type="button" onclick="moveStepMobile(this, -1)">▲</button>
|
||||
<button type="button" onclick="moveStepMobile(this, 1)">▼</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hol-step-row-bottom">
|
||||
<div class="hol-step-meta">
|
||||
<?php if ($isReturn): ?>
|
||||
<span class="hol-tag-return">🏁 Retour</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($step['step_start_date']) && !empty($step['step_end_date'])): ?>
|
||||
<div class="hol-date-weather-wrapper">
|
||||
<span class="hol-step-dates">
|
||||
<?= tr('hdl_from') ?> <?= date('d/m', strtotime($step['step_start_date'])) ?> <?= tr('hdl_to') ?> <?= date('d/m', strtotime($step['step_end_date'])) ?>
|
||||
<span class="hol-step-date">
|
||||
<?= date('d/m', strtotime($step['step_start_date'])) ?> ➔ <?= date('d/m', strtotime($step['step_end_date'])) ?>
|
||||
</span>
|
||||
<div class="hol-weather-info"></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="hol-step-actions">
|
||||
<span class="hol-step-price"><?= number_format($step['total_amount'], 2, ',', ' ') ?> €</span>
|
||||
|
||||
<button onclick="openGpsModal(<?= $step['lat'] ?>, <?= $step['lng'] ?>)"
|
||||
class="btn-icon-small hol-btn-action-icon"
|
||||
title="Y aller avec le GPS">
|
||||
🧭
|
||||
</button>
|
||||
<button onclick="openDocsModal(<?= $step['sort_order'] ?>)" class="btn-icon-small hol-btn-action-icon" title="Documents & Billets">📎</button>
|
||||
|
||||
<button onclick='openCheckpointModal("edit", <?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small hol-btn-action-icon" title="<?= tr('btn_edit') ?>">✏️</button>
|
||||
</div>
|
||||
<div class="hol-cp-actions-group">
|
||||
<div class="hol-cp-total"><?= number_format($step['total_amount'], 2, ',', ' ') ?> €</div>
|
||||
<button onclick='openPlanningModal(<?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="<?= tr('hdl_view_planning') ?>">📅</button>
|
||||
<button onclick='openCheckpointModal("edit", <?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="<?= tr('btn_edit') ?>">✏️</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="hol-cp-body">
|
||||
@@ -238,15 +276,32 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||
if ($it['name'] === 'PF_TECHNICAL_POINT') continue;
|
||||
$visibleItemsCount++;
|
||||
$icon = match($it['category']) { 'transport' => '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' };
|
||||
|
||||
// On vérifie si l'activité possède ses propres coordonnées GPS
|
||||
$hasCustomGps = (!empty($it['lat']) && !empty($it['lng']) && ((float)$it['lat'] !== (float)$step['lat'] || (float)$it['lng'] !== (float)$step['lng']));
|
||||
?>
|
||||
<div class="hol-expense-wrapper">
|
||||
<div class="hol-expense-main">
|
||||
<span class="hol-expense-name"><?= $icon ?> <?= htmlspecialchars($it['name']) ?></span>
|
||||
<span class="hol-expense-name">
|
||||
<?= $icon ?>
|
||||
<?php if ($hasCustomGps): ?>
|
||||
<span onclick="openGpsModal(<?= $it['lat'] ?>, <?= $it['lng'] ?>)" class="hol-custom-gps-link" title="Y aller (GPS)">
|
||||
<?= htmlspecialchars($it['name']) ?>
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<?= htmlspecialchars($it['name']) ?>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<span>
|
||||
<strong class="hol-expense-amount"><?= number_format($it['amount'], 2, ',', ' ') ?> €</strong>
|
||||
<span class="<?= $it['is_paid'] ? 'status-paid' : 'status-pending' ?>" title="<?= $it['is_paid'] ? tr('hdl_paid') : tr('hdl_to_pay') ?>"><?= $it['is_paid'] ? '✓' : '⏳' ?></span>
|
||||
</span>
|
||||
</div>
|
||||
<?php if (!empty($it['notes'])): ?>
|
||||
<div class="hol-expense-note">
|
||||
<?= htmlspecialchars($it['notes']) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
@@ -259,20 +314,21 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pf-card hol-notes-card">
|
||||
<h2 class="pf-card-title">📝 <?= tr('hdl_label_notes') ?></h2>
|
||||
<div class="pf-card-body">
|
||||
<textarea id="holidayGlobalNotes" class="pf-input hol-notes-textarea" rows="6" placeholder="<?= tr('hdl_ph_notes') ?>"><?= htmlspecialchars($holiday['notes'] ?? '') ?></textarea>
|
||||
<div class="hol-notes-actions">
|
||||
<button id="btnSaveHolidayNote" class="pf-btn" onclick="saveHolidayGlobalNote(<?= (int)$_GET['id'] ?>)">
|
||||
💾 <?= tr('btn_save') ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($holiday['notes'])): ?>
|
||||
<div class="hol-summary-card" style="margin-top: 24px; padding: 25px; border-left: 5px solid #f59e0b;">
|
||||
<h3 style="margin: 0 0 15px 0; font-size: 1.2rem; color: #0f172a; display: flex; align-items: center; gap: 8px;">
|
||||
📝 <?= tr('hdl_label_notes') ?>
|
||||
</h3>
|
||||
<div style="font-size: 0.95rem; color: #334155; white-space: pre-wrap; line-height: 1.6;">
|
||||
<?= htmlspecialchars($holiday['notes']) ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="checkpointModal" class="pf-modal">
|
||||
<div class="pf-modal-content" style="max-width: 600px;">
|
||||
@@ -281,6 +337,12 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||
<button type="button" onclick="closeCheckpointModal()" class="pf-modal-close">×</button>
|
||||
</div>
|
||||
|
||||
<!-- LES ONGLETS SONT ICI -->
|
||||
<div style="display: flex; gap: 15px; border-bottom: 1px solid var(--border-light); margin-bottom: 20px;">
|
||||
<button type="button" id="tabBtnInfo" onclick="switchCpTab('info')" style="background: none; border: none; font-weight: bold; font-size: 1rem; color: var(--primary); border-bottom: 2px solid var(--primary); padding-bottom: 8px; cursor: pointer; flex: 1;">📍 Infos de l'étape</button>
|
||||
<button type="button" id="tabBtnProg" onclick="switchCpTab('prog')" style="background: none; border: none; font-weight: bold; font-size: 1rem; color: var(--text-muted); border-bottom: 2px solid transparent; padding-bottom: 8px; cursor: pointer; flex: 1;">🎟️ Programme & Frais</button>
|
||||
</div>
|
||||
|
||||
<div id="cpSearchBlock" style="margin-bottom:20px;">
|
||||
<?php if (!empty($favorites)): ?>
|
||||
<div style="margin-bottom:15px; display:flex; gap:8px; flex-wrap:wrap;">
|
||||
@@ -300,6 +362,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||
<div id="searchResults" style="margin-top:10px; max-height:200px; overflow-y:auto;"></div>
|
||||
</div>
|
||||
|
||||
<!-- FORMULAIRE (Le contenu apparaîtra ici) -->
|
||||
<form action="/modules/holidays/includes/api/save_checkpoint.php" method="POST" id="formCheckpoint" style="display:none; border-top:1px solid #e2e8f0; padding-top:20px;">
|
||||
<input type="hidden" name="holiday_id" value="<?= $id ?>">
|
||||
<input type="hidden" name="old_location_name" id="cp_old_name">
|
||||
@@ -307,35 +370,51 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||
<input type="hidden" name="lng" id="cp_lng">
|
||||
<input type="hidden" name="old_sort_order" id="cp_old_sort_order">
|
||||
|
||||
<!-- CONTENU DE L'ONGLET INFO -->
|
||||
<div id="cpTabInfo">
|
||||
<div class="form-group" style="margin-bottom:15px;">
|
||||
<label class="pf-label"><?= tr('hdl_label_step_name') ?></label>
|
||||
<input type="text" name="location_name" id="cp_name" class="pf-input" style="font-weight:bold; color:var(--primary);" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom:15px; background:#fff7ed; padding:10px; border-radius:8px; border:1px solid #ffedd5;">
|
||||
<label class="pf-label" style="color:#ea580c;">📍 Type d'étape</label>
|
||||
<select name="step_type" id="cp_step_type" class="pf-input" onchange="toggleStepDates(this.value)">
|
||||
<option value="origin">DÉPART (Point de départ du voyage)</option>
|
||||
<option value="stop">SÉJOUR (Étape classique avec arrivée et départ)</option>
|
||||
<option value="destination">ARRIVÉE FINALE (Fin du voyage)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:15px; margin-bottom:15px; background:#f8fafc; padding:12px; border-radius:8px;">
|
||||
<div class="form-group" style="flex:1;">
|
||||
<label class="pf-label"><?= tr('hdl_label_arrival') ?></label>
|
||||
<div class="form-group" id="grp_start_date" style="flex:1;">
|
||||
<label class="pf-label" id="lbl_start_date"><?= tr('hdl_label_arrival') ?></label>
|
||||
<input type="date" name="step_start_date" id="cp_start_date" class="pf-input">
|
||||
</div>
|
||||
<div class="form-group" style="flex:1;">
|
||||
<label class="pf-label"><?= tr('hdl_label_departure') ?></label>
|
||||
<div class="form-group" id="grp_end_date" style="flex:1;">
|
||||
<label class="pf-label" id="lbl_end_date"><?= tr('hdl_label_departure') ?></label>
|
||||
<input type="date" name="step_end_date" id="cp_end_date" class="pf-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display:flex; align-items:center; cursor:pointer; color:#ea580c; font-weight:600;">
|
||||
<input type="checkbox" name="is_return" id="cp_is_return" value="1" style="margin-right:8px;">
|
||||
🏁 <?= tr('hdl_return') ?>
|
||||
<div style="margin-bottom: 20px; padding-top: 15px; border-top: 1px dashed #e2e8f0;">
|
||||
<label style="display:flex; align-items:center; cursor:pointer; color:#ea580c; font-weight:600; font-size:0.9rem;">
|
||||
<input type="checkbox" name="set_as_return" id="cp_set_as_return" value="1" style="margin-right:8px; width:16px; height:16px;">
|
||||
🏁 Définir comme retour
|
||||
</label>
|
||||
<p style="margin: 4px 0 0 24px; font-size: 0.75rem; color: #64748b;">La route sera tracée en orange à partir d'ici.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CONTENU DE L'ONGLET PROGRAMME & FRAIS -->
|
||||
<div id="cpTabProg" class="hol-hidden" style="display: none;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
|
||||
<label class="pf-label" style="margin:0;"><?= tr('hdl_planned_expenses') ?></label>
|
||||
<button type="button" class="pf-btn btn-secondary pf-btn-small" onclick="addCpExpenseLine()"><?= tr('hdl_btn_add_expense') ?></button>
|
||||
</div>
|
||||
|
||||
<div id="cpExpensesContainer" style="margin-bottom:15px;"></div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display:flex; align-items:center; cursor:pointer; font-size:0.85rem; color:#475569;">
|
||||
@@ -354,20 +433,92 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||
</div>
|
||||
|
||||
<div id="planningModal" class="pf-modal">
|
||||
<div class="pf-modal-content" style="max-width: 550px;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||||
<h3 id="planningModalTitle" style="margin:0; color:var(--primary);">📅 <?= tr('hdl_planning_title') ?></h3>
|
||||
<div class="pf-modal-content hol-modal-planning">
|
||||
<div class="hol-modal-header">
|
||||
<h3 id="planningModalTitle" class="hol-modal-title">📅 Planning Global</h3>
|
||||
<button type="button" onclick="closePlanningModal()" class="pf-modal-close">×</button>
|
||||
</div>
|
||||
<div id="planningContainer" style="width: 100%;"></div>
|
||||
|
||||
<!-- Conteneur Injecté en JS -->
|
||||
<div id="planningContainer" class="hol-planning-container"></div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" onclick="closePlanningModal()" class="pf-btn btn-secondary"><?= tr('btn_close') ?></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="transitModal" class="pf-modal">
|
||||
<div class="pf-modal-content hol-modal-sm">
|
||||
<div class="hol-modal-header">
|
||||
<h3 class="hol-modal-title">🛣️ Détail des trajets</h3>
|
||||
<button type="button" onclick="closeTransitModal()" class="pf-modal-close">×</button>
|
||||
</div>
|
||||
<div id="transitDetailsContainer" class="hol-transit-container">
|
||||
<p class="hol-empty-text">Calcul en cours...</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" onclick="closeTransitModal()" class="pf-btn btn-secondary"><?= tr('btn_close') ?></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="gpsModal" class="pf-modal">
|
||||
<div class="pf-modal-content hol-modal-gps">
|
||||
<div class="hol-modal-header">
|
||||
<h3 class="hol-modal-title">🧭 Y aller avec...</h3>
|
||||
<button type="button" onclick="closeGpsModal()" class="pf-modal-close">×</button>
|
||||
</div>
|
||||
|
||||
<div class="hol-gps-buttons">
|
||||
<button onclick="launchGpsApp('waze')" class="pf-btn btn-secondary hol-gps-btn">
|
||||
<img src="https://cdn.simpleicons.org/waze/05C8F0" alt="Waze" class="hol-gps-icon">
|
||||
Waze
|
||||
</button>
|
||||
|
||||
<button onclick="launchGpsApp('gmaps')" class="pf-btn btn-secondary hol-gps-btn">
|
||||
<img src="https://cdn.simpleicons.org/googlemaps" alt="Google Maps" class="hol-gps-icon">
|
||||
Google Maps
|
||||
</button>
|
||||
|
||||
<button onclick="launchGpsApp('amaps')" class="pf-btn btn-secondary hol-gps-btn">
|
||||
<img src="https://cdn.simpleicons.org/apple/000000" alt="Apple Maps" class="hol-gps-icon">
|
||||
Apple Maps
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="docsModal" class="pf-modal">
|
||||
<div class="pf-modal-content hol-modal-docs">
|
||||
<div class="hol-modal-header">
|
||||
<h3 class="hol-modal-title">📎 Porte-documents</h3>
|
||||
<button type="button" onclick="closeDocsModal()" class="pf-modal-close">×</button>
|
||||
</div>
|
||||
|
||||
<div class="hol-docs-dropzone">
|
||||
<p class="hol-docs-hint">Ajoutez vos billets, réservations ou PDFs pour cette étape.</p>
|
||||
|
||||
<input type="file" id="docFileInput" class="hol-hidden" accept=".pdf,.png,.jpg,.jpeg" onchange="handleFileUpload(this)">
|
||||
|
||||
<button type="button" class="pf-btn btn-primary hol-docs-select-btn" onclick="document.getElementById('docFileInput').click()">
|
||||
+ Sélectionner un fichier
|
||||
</button>
|
||||
<div id="uploadStatus" class="hol-upload-status"></div>
|
||||
</div>
|
||||
|
||||
<div id="docsListContainer" class="hol-docs-list">
|
||||
<p class="hol-empty-text">Aucun document pour cette étape.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include __DIR__ . '/modal.php'; ?>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
|
||||
<?php include __DIR__ . '/pdf_template.php'; ?>
|
||||
|
||||
|
||||
<script>
|
||||
// --- 1. SÉCURISATION TRADUCTIONS ET VARIABLES ---
|
||||
window.MAP_POINTS = <?= json_encode($mapPoints ?? []) ?>;
|
||||
@@ -375,46 +526,281 @@ window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
||||
|
||||
window.I18N = {
|
||||
...(window.I18N || {}),
|
||||
'hdl_js_search_loading': "<?= tr('hdl_js_search_loading') ?>",
|
||||
'hdl_js_no_result': "<?= tr('hdl_js_no_result') ?>",
|
||||
'hdl_js_confirm_del_trip': "<?= tr('hdl_js_confirm_del_trip') ?>",
|
||||
'hdl_js_confirm_del_step': "<?= tr('hdl_js_confirm_del_step') ?>",
|
||||
'hdl_js_step_label': "<?= tr('hdl_js_step_label') ?>",
|
||||
'hdl_js_ph_expense_name': "<?= tr('hdl_js_ph_expense_name') ?>",
|
||||
'hdl_js_delete_line': "<?= tr('btn_delete') ?>",
|
||||
'hdl_planning_title': "<?= tr('hdl_planning_title') ?>",
|
||||
'hdl_to_place': "<?= tr('hdl_to_place') ?>",
|
||||
'hdl_js_missing_dates_title': "<?= tr('hdl_js_missing_dates_title') ?>",
|
||||
'hdl_js_missing_dates_msg': "<?= tr('hdl_js_missing_dates_msg') ?>",
|
||||
'hdl_modal_title': "<?= tr('hdl_modal_title') ?>",
|
||||
'hdl_js_edit_step': "<?= tr('hdl_js_edit_step') ?>",
|
||||
'hdl_ph_notes': "<?= tr('hdl_ph_notes') ?>",
|
||||
'hdl_btn_add_step': "<?= tr('hdl_btn_add_step') ?>",
|
||||
'hdl_quick_edit_title': "<?= tr('hdl_quick_edit_title') ?>",
|
||||
'hdl_paid': "<?= tr('hdl_paid') ?>",
|
||||
|
||||
|
||||
// --- NOUVELLES CLÉS MÉTÉO ICI ---
|
||||
'weather_sunny': "<?= tr('weather_sunny') ?>",
|
||||
'weather_cloudy': "<?= tr('weather_cloudy') ?>",
|
||||
'weather_rainy': "<?= tr('weather_rainy') ?>",
|
||||
'weather_snowy': "<?= tr('weather_snowy') ?>",
|
||||
'weather_forecast': "<?= tr('weather_forecast') ?>",
|
||||
'weather_historical': "<?= tr('weather_historical') ?>"
|
||||
'hdl_js_search_loading': <?= json_encode(tr('hdl_js_search_loading')) ?>,
|
||||
'hdl_js_no_result': <?= json_encode(tr('hdl_js_no_result')) ?>,
|
||||
'hdl_js_confirm_del_trip': <?= json_encode(tr('hdl_js_confirm_del_trip')) ?>,
|
||||
'hdl_js_confirm_del_step': <?= json_encode(tr('hdl_js_confirm_del_step')) ?>,
|
||||
'hdl_js_step_label': <?= json_encode(tr('hdl_js_step_label')) ?>,
|
||||
'hdl_js_ph_expense_name': <?= json_encode(tr('hdl_js_ph_expense_name')) ?>,
|
||||
'hdl_js_delete_line': <?= json_encode(tr('btn_delete')) ?>,
|
||||
'hdl_planning_title': <?= json_encode(tr('hdl_planning_title')) ?>,
|
||||
'hdl_to_place': <?= json_encode(tr('hdl_to_place')) ?>,
|
||||
'hdl_js_missing_dates_title': <?= json_encode(tr('hdl_js_missing_dates_title')) ?>,
|
||||
'hdl_js_missing_dates_msg': <?= json_encode(tr('hdl_js_missing_dates_msg')) ?>,
|
||||
'hdl_modal_title': <?= json_encode(tr('hdl_modal_title')) ?>,
|
||||
'hdl_js_edit_step': <?= json_encode(tr('hdl_js_edit_step')) ?>,
|
||||
'hdl_ph_notes': <?= json_encode(tr('hdl_ph_notes')) ?>,
|
||||
'hdl_btn_add_step': <?= json_encode(tr('hdl_btn_add_step')) ?>,
|
||||
'hdl_quick_edit_title': <?= json_encode(tr('hdl_quick_edit_title')) ?>,
|
||||
'hdl_paid': <?= json_encode(tr('hdl_paid')) ?>,
|
||||
'weather_sunny': <?= json_encode(tr('weather_sunny')) ?>,
|
||||
'weather_cloudy': <?= json_encode(tr('weather_cloudy')) ?>,
|
||||
'weather_rainy': <?= json_encode(tr('weather_rainy')) ?>,
|
||||
'weather_snowy': <?= json_encode(tr('weather_snowy')) ?>,
|
||||
'weather_forecast': <?= json_encode(tr('weather_forecast')) ?>,
|
||||
'weather_historical': <?= json_encode(tr('weather_historical')) ?>
|
||||
};
|
||||
|
||||
// Fallback de sécurité pour s'assurer que les modales peuvent toujours se fermer
|
||||
window.closeCheckpointModal = window.closeCheckpointModal || function() {
|
||||
const savedFuelPrice = localStorage.getItem('holidays_fuel_price') || 1.85;
|
||||
window.FUEL_PRICE = parseFloat(savedFuelPrice);
|
||||
|
||||
const displayFuelEl = document.getElementById('display_fuel_price');
|
||||
if (displayFuelEl) displayFuelEl.innerText = window.FUEL_PRICE.toFixed(2);
|
||||
|
||||
window.VEHICLE_CONSUMPTION = <?= !empty($holiday['vehicle_consumption']) ? (float)$holiday['vehicle_consumption'] : 7 ?>;
|
||||
window.GLOBAL_RETURN_STEP_ID = <?= $holiday['return_step_id'] !== null ? $holiday['return_step_id'] : 'null' ?>;
|
||||
|
||||
// --- 2. SURCHARGE SÉCURISÉE DES FONCTIONS JS (APRÈS CHARGEMENT DE HOLIDAYS.JS) ---
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// 1. Correction du conflit d'onglet
|
||||
window.switchCpTab = function(tabId) {
|
||||
const btnInfo = document.getElementById('tabBtnInfo');
|
||||
const btnProg = document.getElementById('tabBtnProg');
|
||||
const tabInfo = document.getElementById('cpTabInfo');
|
||||
const tabProg = document.getElementById('cpTabProg');
|
||||
|
||||
if (tabId === 'info') {
|
||||
if (btnInfo) { btnInfo.style.borderBottomColor = "var(--primary)"; btnInfo.style.color = "var(--primary)"; }
|
||||
if (btnProg) { btnProg.style.borderBottomColor = "transparent"; btnProg.style.color = "var(--text-muted)"; }
|
||||
if (tabInfo) { tabInfo.style.display = "block"; tabInfo.classList.remove("hol-hidden"); }
|
||||
if (tabProg) { tabProg.style.display = "none"; tabProg.classList.add("hol-hidden"); }
|
||||
} else {
|
||||
if (btnProg) { btnProg.style.borderBottomColor = "var(--primary)"; btnProg.style.color = "var(--primary)"; }
|
||||
if (btnInfo) { btnInfo.style.borderBottomColor = "transparent"; btnInfo.style.color = "var(--text-muted)"; }
|
||||
if (tabProg) { tabProg.style.display = "block"; tabProg.classList.remove("hol-hidden"); }
|
||||
if (tabInfo) { tabInfo.style.display = "none"; tabInfo.classList.add("hol-hidden"); }
|
||||
}
|
||||
};
|
||||
|
||||
// 2. Correction modale : ajout de l'injection lat/lng aux items
|
||||
window.openCheckpointModal = function(mode, data = null) {
|
||||
const searchBlock = document.getElementById("cpSearchBlock");
|
||||
const formBlock = document.getElementById("formCheckpoint");
|
||||
const container = document.getElementById("cpExpensesContainer");
|
||||
const btnDel = document.getElementById("btnDeleteCp");
|
||||
const modalTitle = document.getElementById("cpModalTitle");
|
||||
|
||||
if(container) container.innerHTML = "";
|
||||
if (document.getElementById("cp_start_date")) document.getElementById("cp_start_date").value = "";
|
||||
if (document.getElementById("cp_end_date")) document.getElementById("cp_end_date").value = "";
|
||||
if (document.getElementById("searchPlaceInput")) document.getElementById("searchPlaceInput").value = "";
|
||||
if (document.getElementById("searchResults")) document.getElementById("searchResults").innerHTML = "";
|
||||
|
||||
window.switchCpTab('info');
|
||||
|
||||
if (mode === "add") {
|
||||
if (modalTitle) modalTitle.innerText = "📍 " + (window.I18N['hdl_btn_add_step'] || "Ajouter une étape");
|
||||
if (searchBlock) searchBlock.style.display = "block";
|
||||
if (formBlock) formBlock.style.display = "block";
|
||||
if (btnDel) btnDel.style.display = "none";
|
||||
|
||||
if (document.getElementById("cp_old_sort_order")) document.getElementById("cp_old_sort_order").value = "";
|
||||
if (document.getElementById("cp_name")) document.getElementById("cp_name").value = "";
|
||||
|
||||
if (document.getElementById("cp_step_type")) {
|
||||
document.getElementById("cp_step_type").value = "stop";
|
||||
if (typeof toggleStepDates === 'function') toggleStepDates("stop");
|
||||
}
|
||||
if (document.getElementById("cp_set_as_return")) document.getElementById("cp_set_as_return").checked = false;
|
||||
|
||||
if (typeof window.addCpExpenseLine === 'function') window.addCpExpenseLine();
|
||||
|
||||
} else if (mode === "edit" && data) {
|
||||
if (modalTitle) modalTitle.innerText = "✏️ " + (window.I18N['hdl_js_edit_step'] || "Modifier l'étape");
|
||||
if (searchBlock) searchBlock.style.display = "none";
|
||||
if (formBlock) formBlock.style.display = "block";
|
||||
if (btnDel) btnDel.style.display = "block";
|
||||
|
||||
if (document.getElementById("cp_lat")) document.getElementById("cp_lat").value = data.lat;
|
||||
if (document.getElementById("cp_lng")) document.getElementById("cp_lng").value = data.lng;
|
||||
if (document.getElementById("cp_old_sort_order")) document.getElementById("cp_old_sort_order").value = data.sort_order;
|
||||
if (document.getElementById("cp_name")) document.getElementById("cp_name").value = data.location_name;
|
||||
if (document.getElementById("cp_start_date")) document.getElementById("cp_start_date").value = data.step_start_date || "";
|
||||
if (document.getElementById("cp_end_date")) document.getElementById("cp_end_date").value = data.step_end_date || "";
|
||||
|
||||
if (document.getElementById("cp_step_type")) {
|
||||
const type = data.step_type || "stop";
|
||||
document.getElementById("cp_step_type").value = type;
|
||||
if (typeof toggleStepDates === 'function') toggleStepDates(type);
|
||||
}
|
||||
|
||||
if (document.getElementById("cp_set_as_return")) {
|
||||
document.getElementById("cp_set_as_return").checked = (window.GLOBAL_RETURN_STEP_ID == data.sort_order);
|
||||
}
|
||||
|
||||
// Génération avec transmission de la latitude et longitude de chaque dépense
|
||||
if (data.items && data.items.length > 0) {
|
||||
let visibleCount = 0;
|
||||
data.items.forEach((it) => {
|
||||
if (it.name !== "PF_TECHNICAL_POINT") {
|
||||
if (typeof window.addCpExpenseLine === 'function') {
|
||||
window.addCpExpenseLine(
|
||||
it.category, it.name, it.amount, it.is_paid, it.notes || "",
|
||||
it.id || "", it.item_date || "", it.item_time || "", it.duration || 1,
|
||||
it.expense_context || "local", it.lat || "", it.lng || ""
|
||||
);
|
||||
}
|
||||
visibleCount++;
|
||||
}
|
||||
});
|
||||
if (visibleCount === 0 && typeof window.addCpExpenseLine === 'function') window.addCpExpenseLine();
|
||||
} else {
|
||||
if (typeof window.addCpExpenseLine === 'function') window.addCpExpenseLine();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof cpDateRangePicker !== 'undefined' && cpDateRangePicker && typeof cpDateRangePicker.destroy === 'function') {
|
||||
cpDateRangePicker.destroy();
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
const modal = document.getElementById("checkpointModal");
|
||||
if (modal) {
|
||||
modal.style.display = "flex";
|
||||
document.body.classList.add("no-scroll");
|
||||
}
|
||||
};
|
||||
|
||||
// 3. LA FONCTION AVEC LA CLASSE CSS PARFAITE ET LE BOUTON 📍
|
||||
window.addCpExpenseLine = function(category = "accommodation", name = "", amount = "", isPaid = 0, notes = "", itemId = "", itemDate = "", itemTime = "", itemDur = 1, expenseContext = "local", itemLat = "", itemLng = "") {
|
||||
const container = document.getElementById("cpExpensesContainer");
|
||||
if (!container) return;
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.className = "hol-form-row"; // Force l'utilisation du CSS
|
||||
const isChecked = isPaid == 1 ? "checked" : "";
|
||||
|
||||
// Indicateur visuel (bouton vert) si on a déjà des coordonnées pour cette activité
|
||||
const pinBtnStyle = (itemLat && itemLng) ? "background:var(--success);color:white;border-color:var(--success);" : "";
|
||||
|
||||
div.innerHTML = `
|
||||
<div class="hol-form-inner">
|
||||
<select name="items[cat][]" class="pf-input hol-form-select">
|
||||
<option value="accommodation" ${category === "accommodation" ? "selected" : ""}>🏨</option>
|
||||
<option value="transport" ${category === "transport" ? "selected" : ""}>🚗</option>
|
||||
<option value="activity" ${category === "activity" ? "selected" : ""}>🎫</option>
|
||||
</select>
|
||||
<input type="hidden" name="items[context][]" value="${expenseContext}">
|
||||
|
||||
<input type="text" name="items[name][]" class="pf-input hol-form-text" placeholder="${window.I18N['hdl_js_ph_expense_name'] || 'Nom de la dépense'}" value="${name}" required>
|
||||
|
||||
<button type="button" class="btn-icon-action" style="width:28px;height:28px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border-light);border-radius:6px; margin:0; flex-shrink:0; ${pinBtnStyle}" onclick="window.toggleItemSearch(this)" title="Adresse précise">📍</button>
|
||||
|
||||
<input type="number" step="0.01" name="items[amount][]" class="pf-input hol-form-number" placeholder="0.00" value="${amount}" required>
|
||||
|
||||
<label class="hol-form-paid-label" title="${window.I18N['hdl_paid'] || 'Payé'}">
|
||||
<input type="checkbox" ${isChecked} onchange="this.nextElementSibling.value = this.checked ? 1 : 0" style="width:16px;height:16px;margin:0;accent-color:var(--primary);">
|
||||
<input type="hidden" name="items[paid][]" value="${isPaid}">
|
||||
<span class="hol-form-paid-text">Payé</span>
|
||||
</label>
|
||||
|
||||
<button type="button" onclick="this.parentElement.parentElement.remove()" title="${window.I18N['hdl_js_delete_line'] || 'Supprimer'}" class="btn-icon-action delete btn-remove-expense" style="margin: 0;">🗑️</button>
|
||||
</div>
|
||||
|
||||
<div class="hol-item-search-box" style="display:none; padding: 10px; background: var(--bg-subtle); border-radius: 8px; margin-top: 6px; width: 100%;">
|
||||
<div style="display:flex; gap:10px;">
|
||||
<input type="text" class="pf-input item-search-input" placeholder="Chercher une adresse..." onkeypress="if(event.key === 'Enter') { window.searchItemPlace(this); return false; }">
|
||||
<button type="button" class="pf-btn btn-secondary" onclick="window.searchItemPlace(this)">🔍</button>
|
||||
</div>
|
||||
<div class="item-search-results" style="margin-top:10px; max-height:150px; overflow-y:auto;"></div>
|
||||
</div>
|
||||
|
||||
<div class="hol-form-subrow" style="display: flex; padding-left: 58px; margin-top: 6px;">
|
||||
<input type="text" name="items[notes][]" class="pf-input hol-form-notes-input" placeholder="${window.I18N['hdl_ph_notes'] || 'Notes...'}" value="${notes}" style="font-size: 0.8rem; padding: 6px 8px; border-style: dashed; color: var(--text-muted); width: 100%; background: transparent; margin: 0;">
|
||||
</div>
|
||||
<input type="hidden" name="items[id][]" value="${itemId}">
|
||||
<input type="hidden" name="items[date][]" value="${itemDate}">
|
||||
<input type="hidden" name="items[time][]" value="${itemTime}">
|
||||
<input type="hidden" name="items[duration][]" value="${itemDur}">
|
||||
<input type="hidden" name="items[lat][]" value="${itemLat}">
|
||||
<input type="hidden" name="items[lng][]" value="${itemLng}">
|
||||
`;
|
||||
container.appendChild(div);
|
||||
};
|
||||
|
||||
// 4. RÉINTÉGRATION DES FONCTIONS DE GÉOCODAGE DANS LE MÊME SCOPE
|
||||
window.toggleItemSearch = function(btn) {
|
||||
const searchBox = btn.closest(".hol-form-row").querySelector(".hol-item-search-box");
|
||||
searchBox.style.display = searchBox.style.display === "none" ? "block" : "none";
|
||||
};
|
||||
|
||||
window.searchItemPlace = async function(btn) {
|
||||
const container = btn.closest(".hol-item-search-box");
|
||||
const input = container.querySelector(".item-search-input");
|
||||
const resultsDiv = container.querySelector(".item-search-results");
|
||||
const q = input.value.trim();
|
||||
if (q.length < 3) return;
|
||||
|
||||
resultsDiv.innerHTML = `<span style="color:#64748b; font-size:0.85rem;">Recherche en cours...</span>`;
|
||||
|
||||
try {
|
||||
const data = await pachaFetch("/modules/holidays/includes/api/geocode.php?limit=5&q=" + encodeURIComponent(q), { method: 'GET' });
|
||||
resultsDiv.innerHTML = "";
|
||||
if (data.error || !data.results || data.results.length === 0) {
|
||||
resultsDiv.innerHTML = `<span style="color:#ef4444; font-size:0.85rem;">Aucun résultat</span>`;
|
||||
return;
|
||||
}
|
||||
data.results.forEach((place) => {
|
||||
const b = document.createElement("button");
|
||||
b.type = "button";
|
||||
b.className = "pf-btn btn-secondary";
|
||||
b.style.textAlign = "left";
|
||||
b.style.padding = "8px";
|
||||
b.style.marginBottom = "4px";
|
||||
b.style.width = "100%";
|
||||
b.innerText = "📍 " + place.display_name;
|
||||
b.onclick = () => window.selectItemPlace(container, place.lat, place.lng, place.display_name);
|
||||
resultsDiv.appendChild(b);
|
||||
});
|
||||
} catch (err) {
|
||||
resultsDiv.innerHTML = `<span style="color:#ef4444; font-size:0.85rem;">Erreur de connexion</span>`;
|
||||
}
|
||||
};
|
||||
|
||||
window.selectItemPlace = function(container, lat, lng, displayName) {
|
||||
const row = container.closest(".hol-form-row");
|
||||
row.querySelector('input[name="items[lat][]"]').value = lat;
|
||||
row.querySelector('input[name="items[lng][]"]').value = lng;
|
||||
|
||||
// Si le champ note est vide, on l'utilise pour y placer le nom complet de l'adresse
|
||||
const notesInput = row.querySelector('input[name="items[notes][]"]');
|
||||
if (notesInput.value === "") {
|
||||
notesInput.value = displayName;
|
||||
}
|
||||
|
||||
container.style.display = "none";
|
||||
|
||||
// Le bouton devient vert pour confirmer la présence de coordonnées
|
||||
const pinBtn = row.querySelector('.btn-icon-action[title="Adresse précise"]');
|
||||
if (pinBtn) {
|
||||
pinBtn.style.background = "var(--success)";
|
||||
pinBtn.style.color = "white";
|
||||
pinBtn.style.borderColor = "var(--success)";
|
||||
}
|
||||
};
|
||||
|
||||
window.closeCheckpointModal = function() {
|
||||
const modal = document.getElementById('checkpointModal');
|
||||
if(modal) modal.style.display = 'none';
|
||||
document.body.classList.remove('no-scroll');
|
||||
};
|
||||
|
||||
window.closePlanningModal = window.closePlanningModal || function() {
|
||||
window.closePlanningModal = function() {
|
||||
const modal = document.getElementById('planningModal');
|
||||
if(modal) modal.style.display = 'none';
|
||||
document.body.classList.remove('no-scroll');
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<script src="/modules/holidays/holidays.js"></script>
|
||||
@@ -122,9 +122,10 @@ window.closeHolidayModal = window.closeHolidayModal || function() {
|
||||
if(modal) modal.style.display = 'none';
|
||||
document.body.classList.remove('no-scroll');
|
||||
};
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<script src="/modules/holidays/holidays.js"></script>
|
||||
|
||||
<?php
|
||||
function renderHolidayCard($h, $pdo) {
|
||||
@@ -136,8 +137,9 @@ function renderHolidayCard($h, $pdo) {
|
||||
$dateDisplay = htmlspecialchars($h['period_hint'] ?? '');
|
||||
|
||||
if (empty($dateDisplay) && $h['start_date']) {
|
||||
$dateDisplay = date('d/m/Y', strtotime($h['start_date']));
|
||||
if ($h['end_date']) $dateDisplay .= ' → ' . date('d/m/Y', strtotime($h['end_date']));
|
||||
// 💡 Format Jour/Mois uniquement
|
||||
$dateDisplay = date('d/m', strtotime($h['start_date']));
|
||||
if ($h['end_date']) $dateDisplay .= ' → ' . date('d/m', strtotime($h['end_date']));
|
||||
}
|
||||
|
||||
$statusClass = match($h['status']) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div id="holidayModal" class="pf-modal">
|
||||
<div class="pf-modal-content hol-modal-content">
|
||||
<div class="pf-modal-content hol-modal-content" style="max-width: 500px;">
|
||||
<h3 id="modalTitle" class="pf-modal-title"><?= tr('hdl_modal_title') ?></h3>
|
||||
|
||||
<form action="/modules/holidays/includes/api/save_holiday.php" method="POST" id="holidayForm">
|
||||
@@ -24,68 +24,43 @@
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div>
|
||||
<div class="hol-flex-1">
|
||||
<label class="pf-label"><?= tr('hdl_label_period') ?></label>
|
||||
<input type="text" name="period_hint" id="inp_period" class="pf-input" placeholder="<?= tr('hdl_ph_period') ?>">
|
||||
</div>
|
||||
<div class="hol-date-range-group">
|
||||
<div class="hol-flex-1">
|
||||
<label class="pf-label"><?= tr('hdl_label_from') ?></label>
|
||||
<input type="date" name="start_date" id="inp_start" class="pf-input">
|
||||
</div>
|
||||
<div class="hol-flex-1">
|
||||
<label class="pf-label"><?= tr('hdl_label_to') ?></label>
|
||||
<input type="date" name="end_date" id="inp_end" class="pf-input">
|
||||
</div>
|
||||
<!-- DATES AVEC FLATPICKR -->
|
||||
<div class="hol-flex-2">
|
||||
<label class="pf-label">📅 Dates du voyage</label>
|
||||
<input type="text" id="hol_date_range" class="pf-input" placeholder="Sélectionnez les dates..." readonly style="cursor: pointer; background-color: var(--bg-panel); color: var(--primary); font-weight: bold; text-align: center; letter-spacing: 0.5px;">
|
||||
<!-- Vraies données pour save_holiday.php -->
|
||||
<input type="hidden" name="start_date" id="inp_start">
|
||||
<input type="hidden" name="end_date" id="inp_end">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="hol-divider">
|
||||
|
||||
<div class="hol-columns-wrapper">
|
||||
<div class="hol-col">
|
||||
<div class="hol-col-header">
|
||||
<h4 class="hol-cat-transport">🚗 <?= tr('hdl_cat_transport') ?></h4>
|
||||
<button type="button" class="btn-add-item" onclick="addItem('transport')" title="<?= tr('hdl_add_transport') ?>">+</button>
|
||||
</div>
|
||||
<div id="list_transport" class="dynamic-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="hol-col">
|
||||
<div class="hol-col-header">
|
||||
<h4 class="hol-cat-accommodation">🏨 <?= tr('hdl_cat_accommodation') ?></h4>
|
||||
<button type="button" class="btn-add-item" onclick="addItem('accommodation')" title="<?= tr('hdl_add_accommodation') ?>">+</button>
|
||||
</div>
|
||||
<div id="list_accommodation" class="dynamic-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="hol-col">
|
||||
<div class="hol-col-header">
|
||||
<h4 class="hol-cat-activity">🎫 <?= tr('hdl_cat_activity') ?></h4>
|
||||
<button type="button" class="btn-add-item" onclick="addItem('activity')" title="<?= tr('hdl_add_activity') ?>">+</button>
|
||||
</div>
|
||||
<div id="list_activity" class="dynamic-list"></div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top: 15px; padding: 10px; background: var(--bg-subtle); border-radius: 8px; border: 1px solid var(--border-light);">
|
||||
<label class="pf-label" style="margin-bottom: 5px;">🚗 Véhicule utilisé (Optionnel)</label>
|
||||
<select name="vehicle_id" id="inp_vehicle_id" class="pf-input">
|
||||
<option value="">-- Aucun / Autre transport --</option>
|
||||
<?php foreach($garageVehicles as $v): ?>
|
||||
<option value="<?= $v['id'] ?>"><?= htmlspecialchars($v['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<hr class="hol-divider">
|
||||
|
||||
<div class="form-row">
|
||||
<div>
|
||||
<div class="hol-flex-1">
|
||||
<label class="pf-label">🍔 <?= tr('hdl_label_budget_food') ?></label>
|
||||
<input type="number" step="0.01" name="budget_food" id="inp_food" class="pf-input" placeholder="0.00">
|
||||
</div>
|
||||
<div>
|
||||
<div class="hol-flex-1">
|
||||
<label class="pf-label">🎁 <?= tr('hdl_label_budget_extras') ?></label>
|
||||
<input type="number" step="0.01" name="budget_extra" id="inp_extra" class="pf-input" placeholder="0.00">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="pf-label"><?= tr('hdl_label_notes') ?></label>
|
||||
<textarea name="notes" id="inp_notes" class="pf-input" rows="2" placeholder="<?= tr('hdl_ph_notes') ?>"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" onclick="deleteHoliday()" id="btn_delete" class="pf-btn btn-secondary hol-btn-delete"><?= tr('btn_delete') ?></button>
|
||||
<button type="button" onclick="closeHolidayModal()" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
// Calcul du budget global pour la page de synthèse
|
||||
$totalGeneral = $holiday['budget_food'] + $holiday['budget_extra'];
|
||||
foreach ($generalItems as $gi) {
|
||||
$totalGeneral += $gi['amount'];
|
||||
}
|
||||
$totalSteps = 0;
|
||||
foreach ($steps as $step) {
|
||||
$totalSteps += $step['total_amount'];
|
||||
}
|
||||
$grandTotal = $totalGeneral + $totalSteps;
|
||||
?>
|
||||
|
||||
<div style="display: none;">
|
||||
<div id="travelBookTemplate" style="padding: 40px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #1e293b; background: white; width: 100%; max-width: 800px; margin: 0 auto;">
|
||||
|
||||
<div style="text-align: center; padding-top: 50px; margin-bottom: 60px;">
|
||||
<div style="font-size: 4rem; margin-bottom: 20px;">🗺️</div>
|
||||
<h1 style="font-size: 2.5rem; color: #0f172a; margin-bottom: 10px;"><?= htmlspecialchars($holiday['title'] ?? $holiday['name'] ?? 'Mon Voyage') ?></h1>
|
||||
<h2 style="font-size: 1.5rem; color: #64748b; font-weight: normal; margin-top: 0;">
|
||||
<?php if (!empty($holiday['start_date']) && !empty($holiday['end_date'])): ?>
|
||||
Du <?= date('d/m', strtotime($holiday['start_date'])) ?> au <?= date('d/m', strtotime($holiday['end_date'])) ?>
|
||||
<?php else: ?>
|
||||
Dates à définir
|
||||
<?php endif; ?>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 20px; margin-bottom: 40px; page-break-inside: avoid;">
|
||||
<h3 style="margin-top: 0; color: #0f172a; border-bottom: 2px solid #cbd5e1; padding-bottom: 10px;">💰 Budget Prévisionnel : <?= number_format($grandTotal, 2, ',', ' ') ?> €</h3>
|
||||
<table style="width: 100%; border-collapse: collapse; margin-top: 15px;">
|
||||
<tr>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #e2e8f0; color: #475569;">Frais Généraux (Vols, Locations globales...)</td>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #e2e8f0; text-align: right; font-weight: bold;"><?= number_format($totalGeneral, 2, ',', ' ') ?> €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px 0; color: #475569;">Étapes (Hébergements, Activités...)</td>
|
||||
<td style="padding: 8px 0; text-align: right; font-weight: bold;"><?= number_format($totalSteps, 2, ',', ' ') ?> €</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($generalItems) || $holiday['budget_food'] > 0 || $holiday['budget_extra'] > 0): ?>
|
||||
<div style="margin-bottom: 40px; page-break-inside: avoid;">
|
||||
<h3 style="color: #0f172a; border-bottom: 2px solid #0ea5e9; padding-bottom: 5px;">🌍 Réservations & Frais Généraux</h3>
|
||||
<ul style="list-style-type: none; padding-left: 0; margin-top: 15px;">
|
||||
<?php if ($holiday['budget_food'] > 0): ?>
|
||||
<li style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">🍔 Budget Nourriture : <strong><?= number_format($holiday['budget_food'], 2, ',', ' ') ?> €</strong></li>
|
||||
<?php endif; ?>
|
||||
<?php if ($holiday['budget_extra'] > 0): ?>
|
||||
<li style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">🎁 Extras & Souvenirs : <strong><?= number_format($holiday['budget_extra'], 2, ',', ' ') ?> €</strong></li>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($generalItems as $gi):
|
||||
$icon = match($gi['category']) { 'transport' => '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' };
|
||||
?>
|
||||
<li style="padding: 8px 0; border-bottom: 1px solid #f1f5f9; display: flex; justify-content: space-between;">
|
||||
<span><?= $icon ?> <?= htmlspecialchars($gi['name']) ?></span>
|
||||
<span><strong><?= number_format($gi['amount'], 2, ',', ' ') ?> €</strong> <span style="font-size: 0.85em; color: #64748b;"><?= $gi['is_paid'] ? '(Payé ✓)' : '(À payer ⏳)' ?></span></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="page-break-before: always;"></div>
|
||||
<h2 style="color: #0f172a; font-size: 2rem; border-bottom: 3px solid #0f172a; padding-bottom: 10px; margin-bottom: 30px;">📍 Itinéraire Détaillé</h2>
|
||||
|
||||
<?php foreach ($steps as $index => $step): ?>
|
||||
|
||||
<?php if ($index > 0): ?>
|
||||
<div style="text-align: center; padding: 10px 0; color: #94a3b8; page-break-inside: avoid;">
|
||||
<div style="border-left: 2px dashed #cbd5e1; height: 25px; margin: 0 auto; width: 2px;"></div>
|
||||
<div style="margin: 5px 0; font-size: 0.9rem;">🚗 <em>En route vers l'étape suivante...</em></div>
|
||||
<div style="border-left: 2px dashed #cbd5e1; height: 25px; margin: 0 auto; width: 2px;"></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="background: #ffffff; border: 1px solid #e2e8f0; border-left: 6px solid #0ea5e9; border-radius: 8px; padding: 20px; margin-bottom: 0px; page-break-inside: avoid;">
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 15px;">
|
||||
<div>
|
||||
<h3 style="margin: 0; font-size: 1.4rem; color: #0f172a;"><?= htmlspecialchars($step['location_name']) ?></h3>
|
||||
<?php if (!empty($step['step_start_date']) && !empty($step['step_end_date'])): ?>
|
||||
<div style="color: #0ea5e9; margin-top: 5px; font-weight: bold; font-size: 0.95rem;">
|
||||
📅 Du <?= date('d/m', strtotime($step['step_start_date'])) ?> au <?= date('d/m', strtotime($step['step_end_date'])) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<span style="background: #f1f5f9; padding: 5px 10px; border-radius: 6px; font-weight: bold; color: #0f172a;">
|
||||
<?= number_format($step['total_amount'], 2, ',', ' ') ?> €
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php $checkpoints = isset($step['items']) ? $step['items'] : []; ?>
|
||||
|
||||
<?php if (!empty($checkpoints)): ?>
|
||||
<div style="margin-top: 15px; border-top: 1px solid #f8fafc; padding-top: 15px;">
|
||||
<h4 style="margin-top: 0; margin-bottom: 10px; color: #475569; font-size: 1rem;">📋 Planning & Activités :</h4>
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem;">
|
||||
<?php foreach ($checkpoints as $cp):
|
||||
$cpIcon = match($cp['category']) { 'transport' => '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' };
|
||||
// Sécurité sur la date d'activité (item_date ou date)
|
||||
$cpDate = !empty($cp['item_date']) ? $cp['item_date'] : (!empty($cp['date']) ? $cp['date'] : null);
|
||||
?>
|
||||
<tr>
|
||||
<td style="padding: 6px 0; border-bottom: 1px solid #f8fafc; color: #64748b; width: 80px;">
|
||||
<?= $cpDate ? date('d/m', strtotime($cpDate)) : '---' ?>
|
||||
</td>
|
||||
<td style="padding: 6px 0; border-bottom: 1px solid #f8fafc; color: #334155;">
|
||||
<?= $cpIcon ?> <?= htmlspecialchars($cp['name']) ?>
|
||||
</td>
|
||||
<td style="padding: 6px 0; border-bottom: 1px solid #f8fafc; text-align: right; font-weight: bold; color: #0f172a;">
|
||||
<?= number_format($cp['amount'], 2, ',', ' ') ?> €
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<div style="page-break-before: always;"></div>
|
||||
<h2 style="color: #0f172a; font-size: 2rem; border-bottom: 3px solid #0f172a; padding-bottom: 10px; margin-bottom: 30px;">📝 Notes & Informations utiles</h2>
|
||||
|
||||
<p style="color: #64748b; margin-bottom: 40px; font-style: italic;">Espace réservé pour vos numéros d'urgence, codes de cadenas, adresses locales...</p>
|
||||
|
||||
<?php for ($i = 0; $i < 15; $i++): ?>
|
||||
<div style="border-bottom: 1px dotted #cbd5e1; height: 35px; width: 100%;"></div>
|
||||
<?php endfor; ?>
|
||||
|
||||
<div style="text-align: center; margin-top: 60px; font-size: 0.85rem; color: #94a3b8;">
|
||||
Carnet de route généré automatiquement par <strong>HouseHub</strong>. Bon voyage ! ✈️
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
+16
-4
@@ -15,6 +15,7 @@ require_once dirname(__DIR__, 2) . '/includes/db.php';
|
||||
// ── Auto-create tables ────────────────────────────────────────────────────────
|
||||
$pdo->exec("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");
|
||||
|
||||
@@ -48,6 +49,8 @@ $pdo->exec("CREATE TABLE IF NOT EXISTS pf_item_category_rules (
|
||||
try { $pdo->exec("ALTER TABLE pf_grocery_items ADD COLUMN list_id INT NOT NULL DEFAULT 1 AFTER id"); } catch (\Exception $e) {}
|
||||
try { $pdo->exec("ALTER TABLE pf_grocery_items ADD COLUMN category_id INT DEFAULT NULL AFTER list_id"); } catch (\Exception $e) {}
|
||||
try { $pdo->exec("ALTER TABLE pf_grocery_items ADD KEY idx_items_list (list_id)"); } catch (\Exception $e) {}
|
||||
try { $pdo->exec("ALTER TABLE pf_lists ADD COLUMN color VARCHAR(20) DEFAULT NULL AFTER name"); } catch (\Exception $e) {}
|
||||
try { $pdo->exec("ALTER TABLE pf_lists ADD COLUMN list_type VARCHAR(50) DEFAULT NULL AFTER color"); } catch (\Exception $e) {}
|
||||
|
||||
// ── Seed categories if empty ──────────────────────────────────────────────────
|
||||
if ((int)$pdo->query("SELECT COUNT(*) FROM pf_list_categories")->fetchColumn() === 0) {
|
||||
@@ -313,23 +316,32 @@ if ($action === 'set_category' && $method === 'POST') {
|
||||
if ($action === 'lists') {
|
||||
if ($method === 'GET') {
|
||||
$firstId = liste_ensure_default($pdo);
|
||||
$rows = $pdo->query("SELECT id, name, position FROM pf_lists ORDER BY position, id")->fetchAll(PDO::FETCH_ASSOC);
|
||||
$rows = $pdo->query("SELECT id, name, color, list_type, position FROM pf_lists ORDER BY position, id")->fetchAll(PDO::FETCH_ASSOC);
|
||||
echo json_encode(['lists' => $rows, 'default_id' => $firstId]);
|
||||
exit;
|
||||
}
|
||||
if ($method === 'POST') {
|
||||
$name = mb_substr(trim($body['name'] ?? ''), 0, 100);
|
||||
$color = mb_substr(trim($body['color'] ?? ''), 0, 20) ?: null;
|
||||
$list_type = mb_substr(trim($body['list_type'] ?? ''), 0, 50) ?: null;
|
||||
if (!$name) { http_response_code(400); echo json_encode(['error' => 'name required']); exit; }
|
||||
$pos = (int)$pdo->query("SELECT COALESCE(MAX(position),0)+1 FROM pf_lists")->fetchColumn();
|
||||
$pdo->prepare("INSERT INTO pf_lists (name, position) VALUES (?,?)")->execute([$name, $pos]);
|
||||
echo json_encode(['id' => (int)$pdo->lastInsertId(), 'name' => $name, 'position' => $pos]);
|
||||
$pdo->prepare("INSERT INTO pf_lists (name, color, list_type, position) VALUES (?,?,?,?)")->execute([$name, $color, $list_type, $pos]);
|
||||
echo json_encode(['id' => (int)$pdo->lastInsertId(), 'name' => $name, 'color' => $color, 'list_type' => $list_type, 'position' => $pos]);
|
||||
exit;
|
||||
}
|
||||
if ($method === 'PUT') {
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$name = mb_substr(trim($body['name'] ?? ''), 0, 100);
|
||||
$color = array_key_exists('color', $body) ? (mb_substr(trim($body['color'] ?? ''), 0, 20) ?: null) : false;
|
||||
$list_type = array_key_exists('list_type', $body) ? (mb_substr(trim($body['list_type'] ?? ''), 0, 50) ?: null) : false;
|
||||
if (!$id || !$name) { http_response_code(400); echo json_encode(['error' => 'invalid']); exit; }
|
||||
$pdo->prepare("UPDATE pf_lists SET name=? WHERE id=?")->execute([$name, $id]);
|
||||
$sets = ['name=?'];
|
||||
$vals = [$name];
|
||||
if ($color !== false) { $sets[] = 'color=?'; $vals[] = $color; }
|
||||
if ($list_type !== false) { $sets[] = 'list_type=?'; $vals[] = $list_type; }
|
||||
$vals[] = $id;
|
||||
$pdo->prepare("UPDATE pf_lists SET " . implode(', ', $sets) . " WHERE id=?")->execute($vals);
|
||||
echo json_encode(['ok' => true]); exit;
|
||||
}
|
||||
if ($method === 'DELETE') {
|
||||
|
||||
@@ -310,6 +310,135 @@
|
||||
}
|
||||
.liste-empty-icon { font-size: 3rem; margin-bottom: .75rem; }
|
||||
|
||||
/* ── List modal ────────────────────────────────────────────────────────────── */
|
||||
|
||||
.liste-modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.liste-modal {
|
||||
background: var(--card-bg, #fff);
|
||||
border: 1px solid var(--border, #dee2e6);
|
||||
border-radius: 14px;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,.2);
|
||||
}
|
||||
|
||||
.liste-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid var(--border, #dee2e6);
|
||||
}
|
||||
.liste-modal-header h3 { margin: 0; font-size: .95rem; font-weight: 700; color: var(--text, #212529); }
|
||||
|
||||
.liste-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.3rem;
|
||||
cursor: pointer;
|
||||
color: var(--muted, #6c757d);
|
||||
line-height: 1;
|
||||
padding: 0 .25rem;
|
||||
transition: color .15s;
|
||||
}
|
||||
.liste-modal-close:hover { color: var(--text, #212529); }
|
||||
|
||||
.liste-modal-body {
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .85rem;
|
||||
}
|
||||
|
||||
.liste-modal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: .5rem;
|
||||
padding: .85rem 1.25rem;
|
||||
border-top: 1px solid var(--border, #dee2e6);
|
||||
}
|
||||
.liste-modal-footer .btn-tool-danger { margin-right: auto; }
|
||||
|
||||
.liste-form-group { display: flex; flex-direction: column; gap: .3rem; }
|
||||
.liste-form-label {
|
||||
font-size: .73rem;
|
||||
font-weight: 700;
|
||||
color: var(--muted, #6c757d);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .04em;
|
||||
}
|
||||
|
||||
.liste-modal-name, .liste-modal-type {
|
||||
padding: .5rem .7rem;
|
||||
border: 1px solid var(--border, #dee2e6);
|
||||
border-radius: 8px;
|
||||
background: var(--input-bg, #fff);
|
||||
color: var(--text, #212529);
|
||||
font-size: .875rem;
|
||||
font-family: inherit;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
transition: border-color .15s;
|
||||
}
|
||||
.liste-modal-name:focus, .liste-modal-type:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary, #4361ee);
|
||||
box-shadow: 0 0 0 3px rgba(67,97,238,.15);
|
||||
}
|
||||
|
||||
.btn-tool-primary {
|
||||
background: var(--primary, #4361ee);
|
||||
color: #fff;
|
||||
border-color: var(--primary, #4361ee);
|
||||
}
|
||||
.btn-tool-primary:hover { filter: brightness(1.1); background: var(--primary, #4361ee); }
|
||||
|
||||
/* ── Color picker ──────────────────────────────────────────────────────────── */
|
||||
|
||||
.liste-color-picker {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .45rem;
|
||||
}
|
||||
|
||||
.liste-color-swatch {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
background: var(--swatch, transparent);
|
||||
cursor: pointer;
|
||||
transition: transform .1s;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.liste-color-swatch:hover { transform: scale(1.2); }
|
||||
.liste-color-swatch.active {
|
||||
outline-color: var(--swatch, var(--primary, #4361ee));
|
||||
border-color: var(--card-bg, #fff);
|
||||
}
|
||||
.liste-color-swatch-none {
|
||||
background: transparent !important;
|
||||
border: 2px dashed var(--border, #dee2e6) !important;
|
||||
}
|
||||
.liste-color-swatch-none.active { outline-color: var(--primary, #4361ee); }
|
||||
|
||||
/* ── Tab type emoji ────────────────────────────────────────────────────────── */
|
||||
|
||||
.liste-tab-type { font-size: .85rem; line-height: 1; }
|
||||
|
||||
/* ── Category section headers ──────────────────────────────────────────────── */
|
||||
|
||||
.liste-cat-section-header {
|
||||
@@ -429,6 +558,13 @@
|
||||
[data-theme='dark'] .btn-tool:hover { background: #2a2a3e; }
|
||||
[data-theme='dark'] .liste-new-form input { background: #1e1e2e; color: #cdd6f4; border-color: #4361ee; }
|
||||
[data-theme='dark'] .liste-new-form #btn-cancel-new-list { background: #2a2a3e; color: #cdd6f4; }
|
||||
[data-theme='dark'] .liste-modal { background: #1e1e2e; border-color: #3a3a4e; }
|
||||
[data-theme='dark'] .liste-modal-header,
|
||||
[data-theme='dark'] .liste-modal-footer { border-color: #3a3a4e; }
|
||||
[data-theme='dark'] .liste-modal-header h3 { color: #cdd6f4; }
|
||||
[data-theme='dark'] .liste-modal-name,
|
||||
[data-theme='dark'] .liste-modal-type { background: #2a2a3e; border-color: #3a3a4e; color: #cdd6f4; }
|
||||
[data-theme='dark'] .liste-color-swatch.active { border-color: #1e1e2e; }
|
||||
[data-theme='dark'] .liste-cat-badge { border-color: #3a3a4e; }
|
||||
[data-theme='dark'] .liste-cat-badge:hover { background: #313155; border-color: #4361ee; }
|
||||
[data-theme='dark'] .liste-cat-section-count { background: #2a2a3e; color: #8892b0; }
|
||||
|
||||
+168
-65
@@ -11,6 +11,35 @@ const state = {
|
||||
catMap: {},
|
||||
};
|
||||
|
||||
// ── Constants ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const LIST_COLORS = [
|
||||
{ id: 'none', hex: null },
|
||||
{ id: 'blue', hex: '#3b82f6' },
|
||||
{ id: 'red', hex: '#ef4444' },
|
||||
{ id: 'amber', hex: '#f59e0b' },
|
||||
{ id: 'purple', hex: '#8b5cf6' },
|
||||
{ id: 'pink', hex: '#ec4899' },
|
||||
{ id: 'emerald', hex: '#10b981' },
|
||||
{ id: 'orange', hex: '#f97316' },
|
||||
{ id: 'cyan', hex: '#06b6d4' },
|
||||
{ id: 'indigo', hex: '#6366f1' },
|
||||
{ id: 'teal', hex: '#14b8a6' },
|
||||
{ id: 'gray', hex: '#6b7280' },
|
||||
];
|
||||
|
||||
const LIST_TYPES = [
|
||||
{ value: '', label: '— Aucun —', emoji: '' },
|
||||
{ value: 'courses', label: 'Courses', emoji: '🛒' },
|
||||
{ value: 'todo', label: 'To-do', emoji: '✅' },
|
||||
{ value: 'voyage', label: 'Voyage', emoji: '✈️' },
|
||||
{ value: 'travail', label: 'Travail', emoji: '💼' },
|
||||
{ value: 'maison', label: 'Maison', emoji: '🏠' },
|
||||
{ value: 'sante', label: 'Santé', emoji: '💊' },
|
||||
{ value: 'loisirs', label: 'Loisirs', emoji: '🎮' },
|
||||
{ value: 'autre', label: 'Autre', emoji: '📋' },
|
||||
];
|
||||
|
||||
// ── Utilities ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function esc(s) {
|
||||
@@ -42,6 +71,116 @@ async function api(action, method = 'GET', body = null, params = {}) {
|
||||
return r.json();
|
||||
}
|
||||
|
||||
// ── List modal ─────────────────────────────────────────────────────────────────
|
||||
|
||||
let activeModal = null;
|
||||
|
||||
function openListModal(listId = null) {
|
||||
if (activeModal) { activeModal.remove(); activeModal = null; }
|
||||
const list = listId ? state.lists.find(l => l.id === listId) : null;
|
||||
const isNew = !listId;
|
||||
|
||||
const backdrop = document.createElement('div');
|
||||
backdrop.className = 'liste-modal-backdrop';
|
||||
backdrop.innerHTML = `
|
||||
<div class="liste-modal" role="dialog" aria-modal="true">
|
||||
<div class="liste-modal-header">
|
||||
<h3>${isNew ? 'Nouvelle liste' : 'Modifier la liste'}</h3>
|
||||
<button class="liste-modal-close" aria-label="Fermer">×</button>
|
||||
</div>
|
||||
<div class="liste-modal-body">
|
||||
<div class="liste-form-group">
|
||||
<label class="liste-form-label">Nom</label>
|
||||
<input class="liste-modal-name" type="text" maxlength="100"
|
||||
value="${esc(list?.name ?? '')}" placeholder="Ma liste…" autocomplete="off">
|
||||
</div>
|
||||
<div class="liste-form-group">
|
||||
<label class="liste-form-label">Couleur</label>
|
||||
<div class="liste-color-picker">
|
||||
${LIST_COLORS.map(c => {
|
||||
const isActive = (c.hex === null && !list?.color) || (list?.color === c.hex);
|
||||
const cls = 'liste-color-swatch' + (c.hex === null ? ' liste-color-swatch-none' : '') + (isActive ? ' active' : '');
|
||||
const style = c.hex ? `style="--swatch:${c.hex}"` : '';
|
||||
return `<button class="${cls}" data-color="${c.hex ?? ''}" ${style} title="${c.id}"></button>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="liste-form-group">
|
||||
<label class="liste-form-label">Type de liste</label>
|
||||
<select class="liste-modal-type">
|
||||
${LIST_TYPES.map(t =>
|
||||
`<option value="${esc(t.value)}" ${list?.list_type === t.value ? 'selected' : ''}>${t.emoji ? t.emoji + ' ' : ''}${esc(t.label)}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="liste-modal-footer">
|
||||
${!isNew && state.lists.length > 1
|
||||
? `<button class="btn-tool btn-tool-danger liste-modal-delete">Supprimer</button>`
|
||||
: ''}
|
||||
<button class="btn-tool liste-modal-cancel">Annuler</button>
|
||||
<button class="btn-tool btn-tool-primary liste-modal-save">Enregistrer</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
document.body.appendChild(backdrop);
|
||||
activeModal = backdrop;
|
||||
|
||||
const nameInput = backdrop.querySelector('.liste-modal-name');
|
||||
nameInput.focus();
|
||||
nameInput.setSelectionRange(nameInput.value.length, nameInput.value.length);
|
||||
|
||||
// Color swatches
|
||||
backdrop.querySelectorAll('.liste-color-swatch').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
backdrop.querySelectorAll('.liste-color-swatch').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
const close = () => { backdrop.remove(); activeModal = null; };
|
||||
backdrop.querySelector('.liste-modal-close').addEventListener('click', close);
|
||||
backdrop.querySelector('.liste-modal-cancel').addEventListener('click', close);
|
||||
backdrop.addEventListener('click', e => { if (e.target === backdrop) close(); });
|
||||
|
||||
backdrop.querySelector('.liste-modal-delete')?.addEventListener('click', () => { close(); deleteList(listId); });
|
||||
backdrop.querySelector('.liste-modal-save').addEventListener('click', () => saveListModal(backdrop, listId));
|
||||
nameInput.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') saveListModal(backdrop, listId);
|
||||
if (e.key === 'Escape') close();
|
||||
});
|
||||
}
|
||||
|
||||
async function saveListModal(backdrop, listId) {
|
||||
const name = backdrop.querySelector('.liste-modal-name').value.trim();
|
||||
if (!name) { backdrop.querySelector('.liste-modal-name').focus(); return; }
|
||||
|
||||
const activeSwatch = backdrop.querySelector('.liste-color-swatch.active');
|
||||
const color = activeSwatch?.dataset.color || null;
|
||||
const list_type = backdrop.querySelector('.liste-modal-type').value || null;
|
||||
|
||||
if (listId) {
|
||||
const r = await api('lists', 'PUT', { name, color, list_type }, { id: listId });
|
||||
if (r.ok) {
|
||||
const list = state.lists.find(l => l.id === listId);
|
||||
if (list) { list.name = name; list.color = color; list.list_type = list_type; }
|
||||
backdrop.remove(); activeModal = null;
|
||||
renderTabs();
|
||||
toast('Liste mise à jour');
|
||||
}
|
||||
} else {
|
||||
const r = await api('lists', 'POST', { name, color, list_type });
|
||||
if (r.id) {
|
||||
state.lists.push({ id: r.id, name, color: r.color, list_type: r.list_type, position: r.position });
|
||||
state.currentListId = r.id;
|
||||
backdrop.remove(); activeModal = null;
|
||||
await loadItems();
|
||||
renderTabs();
|
||||
toast('Liste créée : ' + name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Categories ─────────────────────────────────────────────────────────────────
|
||||
|
||||
async function loadCategories() {
|
||||
@@ -104,49 +243,54 @@ function closeCategoryPicker() {
|
||||
|
||||
// ── Tabs ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
function typeEmoji(list_type) {
|
||||
return LIST_TYPES.find(t => t.value === list_type)?.emoji || '';
|
||||
}
|
||||
|
||||
function renderTabs() {
|
||||
const container = document.getElementById('liste-tabs');
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
state.lists.forEach(list => {
|
||||
const isActive = list.id === state.currentListId;
|
||||
const tab = document.createElement('div');
|
||||
tab.className = 'liste-tab' + (list.id === state.currentListId ? ' active' : '');
|
||||
tab.className = 'liste-tab' + (isActive ? ' active' : '');
|
||||
tab.dataset.id = list.id;
|
||||
|
||||
if (list.id === state.currentListId) {
|
||||
// Apply custom color to entire tab
|
||||
if (list.color) {
|
||||
if (isActive) {
|
||||
tab.style.cssText = `background:${list.color};border-color:${list.color};color:#fff`;
|
||||
} else {
|
||||
tab.style.cssText = `border-color:${list.color};color:${list.color}`;
|
||||
}
|
||||
}
|
||||
|
||||
const emoji = typeEmoji(list.list_type);
|
||||
const prefix = emoji ? `<span class="liste-tab-type">${emoji}</span>` : '';
|
||||
|
||||
if (isActive) {
|
||||
tab.innerHTML = `
|
||||
${prefix}
|
||||
<span class="liste-tab-name">${esc(list.name)}</span>
|
||||
<button class="liste-tab-btn liste-tab-rename" title="${esc(T.rename_list)}" data-id="${list.id}">✏</button>
|
||||
${state.lists.length > 1 ? `<button class="liste-tab-btn liste-tab-delete" title="Supprimer" data-id="${list.id}">×</button>` : ''}
|
||||
<button class="liste-tab-btn liste-tab-edit" title="Modifier" data-id="${list.id}">✏</button>
|
||||
`;
|
||||
} else {
|
||||
tab.innerHTML = `<span class="liste-tab-name">${esc(list.name)}</span>`;
|
||||
tab.innerHTML = `${prefix}<span class="liste-tab-name">${esc(list.name)}</span>`;
|
||||
tab.addEventListener('click', () => switchList(list.id));
|
||||
}
|
||||
container.appendChild(tab);
|
||||
});
|
||||
|
||||
container.querySelectorAll('.liste-tab-rename').forEach(btn => {
|
||||
btn.addEventListener('click', e => { e.stopPropagation(); startRename(parseInt(btn.dataset.id)); });
|
||||
});
|
||||
container.querySelectorAll('.liste-tab-delete').forEach(btn => {
|
||||
btn.addEventListener('click', e => { e.stopPropagation(); deleteList(parseInt(btn.dataset.id)); });
|
||||
container.querySelectorAll('.liste-tab-edit').forEach(btn => {
|
||||
btn.addEventListener('click', e => { e.stopPropagation(); openListModal(parseInt(btn.dataset.id)); });
|
||||
});
|
||||
}
|
||||
|
||||
function startRename(listId) {
|
||||
const list = state.lists.find(l => l.id === listId);
|
||||
if (!list) return;
|
||||
const name = prompt(T.new_name || 'Nouveau nom :', list.name);
|
||||
if (name === null || name.trim() === '') return;
|
||||
api('lists', 'PUT', { name: name.trim() }, { id: listId }).then(r => {
|
||||
if (r.ok) {
|
||||
list.name = name.trim();
|
||||
renderTabs();
|
||||
toast(T.list_renamed || 'Liste renommée');
|
||||
}
|
||||
});
|
||||
}
|
||||
// ── List management ─────────────────────────────────────────────────────────────
|
||||
|
||||
document.getElementById('btn-add-list')?.addEventListener('click', () => openListModal(null));
|
||||
|
||||
async function deleteList(listId) {
|
||||
if (!confirm(T.confirm_delete_list || 'Supprimer cette liste et tous ses articles ?')) return;
|
||||
@@ -161,41 +305,6 @@ async function deleteList(listId) {
|
||||
toast(T.list_deleted || 'Liste supprimée');
|
||||
}
|
||||
|
||||
// ── List management ─────────────────────────────────────────────────────────────
|
||||
|
||||
document.getElementById('btn-add-list')?.addEventListener('click', () => {
|
||||
document.getElementById('liste-new-form')?.classList.remove('hidden');
|
||||
document.getElementById('btn-add-list')?.classList.add('hidden');
|
||||
document.getElementById('new-list-name')?.focus();
|
||||
});
|
||||
document.getElementById('btn-cancel-new-list')?.addEventListener('click', cancelNewList);
|
||||
document.getElementById('btn-confirm-new-list')?.addEventListener('click', confirmNewList);
|
||||
document.getElementById('new-list-name')?.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') confirmNewList();
|
||||
if (e.key === 'Escape') cancelNewList();
|
||||
});
|
||||
|
||||
function cancelNewList() {
|
||||
document.getElementById('liste-new-form')?.classList.add('hidden');
|
||||
document.getElementById('btn-add-list')?.classList.remove('hidden');
|
||||
if (document.getElementById('new-list-name')) document.getElementById('new-list-name').value = '';
|
||||
}
|
||||
|
||||
async function confirmNewList() {
|
||||
const input = document.getElementById('new-list-name');
|
||||
const name = input?.value.trim();
|
||||
if (!name) return;
|
||||
const r = await api('lists', 'POST', { name });
|
||||
if (r.id) {
|
||||
state.lists.push({ id: r.id, name: r.name, position: r.position });
|
||||
state.currentListId = r.id;
|
||||
cancelNewList();
|
||||
await loadItems();
|
||||
renderTabs();
|
||||
toast((T.list_created || 'Liste créée') + ' : ' + r.name);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Switch list ─────────────────────────────────────────────────────────────────
|
||||
|
||||
async function switchList(listId) {
|
||||
@@ -207,10 +316,7 @@ async function switchList(listId) {
|
||||
// ── Items ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function loadItems() {
|
||||
if (!state.currentListId) {
|
||||
renderItems();
|
||||
return;
|
||||
}
|
||||
if (!state.currentListId) { renderItems(); return; }
|
||||
const r = await api('items', 'GET', null, { list_id: state.currentListId });
|
||||
state.items = r.items || [];
|
||||
renderItems();
|
||||
@@ -236,15 +342,12 @@ function renderItems() {
|
||||
let html = '';
|
||||
|
||||
if (hasCategories && state.categories.length) {
|
||||
// Group pending items by category
|
||||
const groups = new Map();
|
||||
pending.forEach(item => {
|
||||
const key = item.category_id ?? 0;
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key).push(item);
|
||||
});
|
||||
|
||||
// Render in category order, uncategorized last
|
||||
const catIds = state.categories.map(c => parseInt(c.id));
|
||||
const orderedKeys = catIds.filter(id => groups.has(id));
|
||||
if (groups.has(0)) orderedKeys.push(0);
|
||||
|
||||
@@ -153,7 +153,7 @@ function renderAttachmentsView(atts,noteId){
|
||||
<div class="memo-att-title">🖼️ Images</div>
|
||||
<div class="att-img-grid">${images.map(a=>`
|
||||
<div class="att-img-item" onclick="openLightbox('${API}?action=file&id=${a.id}')">
|
||||
<img src="${API}?action=file&id=${a.id}" loading="lazy" alt="${escHtml(a.original_name||'')}">
|
||||
<img src="${API}?action=file&id=${a.id}" alt="${escHtml(a.original_name||'')}">
|
||||
<button class="att-del" onclick="event.stopPropagation();deleteAttachment(${a.id},${noteId})" title="Supprimer">✕</button>
|
||||
</div>`).join('')}</div></div>`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
// modules/voyage/api.php
|
||||
// Toll cost estimator API for HouseHub
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/includes/auth.php';
|
||||
require_login();
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
if ($action !== 'estimate') {
|
||||
echo json_encode(['ok' => false, 'error' => 'Action inconnue']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- Read POST body ---
|
||||
$body = file_get_contents('php://input');
|
||||
$params = json_decode($body, true);
|
||||
if (!$params || empty($params['stops']) || count($params['stops']) < 2) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Paramètres invalides (stops requis, min 2)']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stops = $params['stops'];
|
||||
$fuelL100 = (float)($params['fuel_l100'] ?? 7);
|
||||
$fuelPrice= (float)($params['fuel_price'] ?? 1.85);
|
||||
|
||||
// --- Load pre-indexed PHP database ---
|
||||
$tollsDataPath = __DIR__ . '/data/tolls.data.php';
|
||||
$gpsPath = __DIR__ . '/data/toll_gps.json';
|
||||
|
||||
if (!file_exists($tollsDataPath)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Base de péages introuvable']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Grâce à OPcache, le fichier est exécuté et stocké en RAM partagée
|
||||
$tollIndex = require $tollsDataPath;
|
||||
|
||||
$gpsData = [];
|
||||
if (file_exists($gpsPath)) {
|
||||
$gpsData = json_decode(file_get_contents($gpsPath), true) ?? [];
|
||||
}
|
||||
|
||||
// --- Helper: great-circle distance (km) ---
|
||||
function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float {
|
||||
$R = 6371.0;
|
||||
$dLat = deg2rad($lat2 - $lat1);
|
||||
$dLng = deg2rad($lng2 - $lng1);
|
||||
$a = sin($dLat/2)**2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng/2)**2;
|
||||
return $R * 2 * atan2(sqrt($a), sqrt(1 - $a));
|
||||
}
|
||||
|
||||
// --- Find plazas near a coordinate (within $maxKm) ---
|
||||
// Returns array of [plaza_name => distance_km]
|
||||
function plazasNear(array $gpsData, float $lat, float $lng, float $maxKm): array {
|
||||
$result = [];
|
||||
foreach ($gpsData as $name => $info) {
|
||||
$d = haversine($lat, $lng, $info['lat'], $info['lng']);
|
||||
if ($d <= $maxKm) {
|
||||
$result[$name] = $d;
|
||||
}
|
||||
}
|
||||
asort($result);
|
||||
return $result;
|
||||
}
|
||||
|
||||
// --- Look up toll price for (op, entry, exit) with fallback ---
|
||||
function lookupToll(array $tollIndex, string $op, string $entry, string $exit): ?float {
|
||||
// Direct lookup
|
||||
if (isset($tollIndex[$op][$entry][$exit])) {
|
||||
return $tollIndex[$op][$entry][$exit];
|
||||
}
|
||||
// Reversed
|
||||
if (isset($tollIndex[$op][$exit][$entry])) {
|
||||
return $tollIndex[$op][$exit][$entry];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- OSRM route distance ---
|
||||
function getRouteDistanceKm(float $lat1, float $lng1, float $lat2, float $lng2): ?float {
|
||||
$url = sprintf(
|
||||
'https://router.project-osrm.org/route/v1/driving/%f,%f;%f,%f?overview=false',
|
||||
$lng1, $lat1, $lng2, $lat2
|
||||
);
|
||||
$ctx = stream_context_create([
|
||||
'http' => [
|
||||
'timeout' => 10,
|
||||
'header' => "User-Agent: HouseHub/1.0\r\n"
|
||||
]
|
||||
]);
|
||||
$resp = @file_get_contents($url, false, $ctx);
|
||||
if ($resp === false) return null;
|
||||
$data = json_decode($resp, true);
|
||||
if (!empty($data['routes'][0]['distance'])) {
|
||||
return (float)$data['routes'][0]['distance'] / 1000.0;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- Main: process each consecutive stop pair ---
|
||||
$segments = [];
|
||||
$totalKm = 0.0;
|
||||
$totalToll = 0.0;
|
||||
|
||||
for ($i = 0; $i < count($stops) - 1; $i++) {
|
||||
$orig = $stops[$i];
|
||||
$dest = $stops[$i + 1];
|
||||
|
||||
$oLat = (float)$orig['lat'];
|
||||
$oLng = (float)$orig['lng'];
|
||||
$dLat = (float)$dest['lat'];
|
||||
$dLng = (float)$dest['lng'];
|
||||
$fromName = $orig['name'] ?? "Étape " . ($i + 1);
|
||||
$toName = $dest['name'] ?? "Étape " . ($i + 2);
|
||||
|
||||
// Distance via OSRM
|
||||
$distKm = getRouteDistanceKm($oLat, $oLng, $dLat, $dLng);
|
||||
if ($distKm === null) {
|
||||
// Fallback: straight-line distance * 1.25
|
||||
$distKm = haversine($oLat, $oLng, $dLat, $dLng) * 1.25;
|
||||
}
|
||||
$totalKm += $distKm;
|
||||
|
||||
// --- Find toll ---
|
||||
$segToll = 0.0;
|
||||
$entryPlaza = null;
|
||||
$exitPlaza = null;
|
||||
$tollNote = null;
|
||||
|
||||
if (!empty($gpsData)) {
|
||||
// Find plazas near origin (80km) and destination (80km)
|
||||
$nearOrig = plazasNear($gpsData, $oLat, $oLng, 80.0);
|
||||
$nearDest = plazasNear($gpsData, $dLat, $dLng, 80.0);
|
||||
|
||||
$bestToll = null;
|
||||
$bestDistSum = PHP_FLOAT_MAX;
|
||||
$bestEntry = null;
|
||||
$bestExit = null;
|
||||
$bestOp = null;
|
||||
|
||||
// Try each operator
|
||||
foreach ($tollIndex as $op => $opEntries) {
|
||||
// Find candidate entry plazas (near origin, present in this operator)
|
||||
$candidateEntries = [];
|
||||
foreach ($nearOrig as $pName => $dist) {
|
||||
if (isset($opEntries[$pName]) || isset(array_flip(array_keys($opEntries))[$pName])) {
|
||||
// Plaza exists as entry in this operator
|
||||
if (isset($opEntries[$pName])) {
|
||||
$candidateEntries[$pName] = $dist;
|
||||
}
|
||||
}
|
||||
// Also check if it's an exit plaza (reversed lookup later)
|
||||
// Include any plaza from this operator that is within range
|
||||
}
|
||||
|
||||
// Build set of all plazas for this operator
|
||||
$opPlazas = [];
|
||||
foreach ($opEntries as $entry => $exits) {
|
||||
$opPlazas[$entry] = true;
|
||||
foreach ($exits as $exit => $price) {
|
||||
$opPlazas[$exit] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter nearOrig and nearDest to only plazas in this operator
|
||||
$opNearOrig = array_intersect_key($nearOrig, $opPlazas);
|
||||
$opNearDest = array_intersect_key($nearDest, $opPlazas);
|
||||
|
||||
if (empty($opNearOrig) || empty($opNearDest)) continue;
|
||||
|
||||
// Try best 5 entry and best 5 exit candidates
|
||||
$topEntries = array_slice($opNearOrig, 0, 5, true);
|
||||
$topExits = array_slice($opNearDest, 0, 5, true);
|
||||
|
||||
foreach ($topEntries as $ePlaza => $eDist) {
|
||||
foreach ($topExits as $xPlaza => $xDist) {
|
||||
if ($ePlaza === $xPlaza) continue;
|
||||
$price = lookupToll($tollIndex, $op, $ePlaza, $xPlaza);
|
||||
if ($price !== null) {
|
||||
$distSum = $eDist + $xDist;
|
||||
if ($distSum < $bestDistSum) {
|
||||
$bestDistSum = $distSum;
|
||||
$bestToll = $price;
|
||||
$bestEntry = $ePlaza;
|
||||
$bestExit = $xPlaza;
|
||||
$bestOp = $op;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($bestToll !== null) {
|
||||
$segToll = $bestToll;
|
||||
$entryPlaza = $bestEntry;
|
||||
$exitPlaza = $bestExit;
|
||||
} else {
|
||||
$tollNote = 'péages non estimés pour ce trajet';
|
||||
}
|
||||
} else {
|
||||
$tollNote = 'base GPS des péages non disponible';
|
||||
}
|
||||
|
||||
$totalToll += $segToll;
|
||||
|
||||
$seg = [
|
||||
'from' => $fromName,
|
||||
'to' => $toName,
|
||||
'distance_km' => round($distKm, 1),
|
||||
'toll' => round($segToll, 2),
|
||||
];
|
||||
if ($entryPlaza) $seg['entry_plaza'] = $entryPlaza;
|
||||
if ($exitPlaza) $seg['exit_plaza'] = $exitPlaza;
|
||||
if ($bestOp) $seg['op'] = $bestOp;
|
||||
if ($tollNote) $seg['note'] = $tollNote;
|
||||
|
||||
$segments[] = $seg;
|
||||
}
|
||||
|
||||
$fuelCost = round(($totalKm / 100.0) * $fuelL100 * $fuelPrice, 2);
|
||||
$grandTotal = round($totalToll + $fuelCost, 2);
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'segments' => $segments,
|
||||
'total_km' => round($totalKm, 1),
|
||||
'total_toll' => round($totalToll, 2),
|
||||
'fuel_cost' => $fuelCost,
|
||||
'grand_total' => $grandTotal,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+57
-6
@@ -38,6 +38,47 @@ function createFamilyDb(PDO $meta, string $db_host, string $db_user, string $db_
|
||||
return $db_name;
|
||||
}
|
||||
|
||||
function generateCalendarWeeks(PDO $familyPdo) {
|
||||
$monthsFr = [1=>'Janvier', 2=>'Février', 3=>'Mars', 4=>'Avril', 5=>'Mai', 6=>'Juin', 7=>'Juillet', 8=>'Août', 9=>'Septembre', 10=>'Octobre', 11=>'Novembre', 12=>'Décembre'];
|
||||
|
||||
$sql = "REPLACE INTO pf_calendar_weeks
|
||||
(year, week_iso_year, week_iso_number, week_label, month, month_name, week_start_date, mon_date, tue_date, wed_date, thu_date, fri_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
$stmt = $familyPdo->prepare($sql);
|
||||
|
||||
// On commence de l'année précédente jusqu'à +10 ans
|
||||
$currentYear = (int)date('Y');
|
||||
$startDate = new DateTime(($currentYear - 1) . '-08-28'); // On s'assure de couvrir la rentrée scolaire précédente
|
||||
$endDate = new DateTime(($currentYear + 10) . '-12-31');
|
||||
|
||||
while ($startDate <= $endDate) {
|
||||
$mon = clone $startDate;
|
||||
$tue = clone $startDate; $tue->modify('+1 day');
|
||||
$wed = clone $startDate; $wed->modify('+2 days');
|
||||
$thu = clone $startDate; $thu->modify('+3 days');
|
||||
$fri = clone $startDate; $fri->modify('+4 days');
|
||||
|
||||
$month = (int)$mon->format('n');
|
||||
|
||||
$stmt->execute([
|
||||
(int)$mon->format('Y'),
|
||||
(int)$mon->format('o'),
|
||||
(int)$mon->format('W'),
|
||||
"Semaine " . (int)$mon->format('W'),
|
||||
$month,
|
||||
$monthsFr[$month],
|
||||
$mon->format('Y-m-d'),
|
||||
$mon->format('Y-m-d'),
|
||||
$tue->format('Y-m-d'),
|
||||
$wed->format('Y-m-d'),
|
||||
$thu->format('Y-m-d'),
|
||||
$fri->format('Y-m-d')
|
||||
]);
|
||||
|
||||
$startDate->modify('+1 week');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Traitement du formulaire ─────────────────────────────────────────────────
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
@@ -90,13 +131,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
"INSERT INTO users (username, password_hash, display_name, family_id) VALUES (?, ?, ?, ?)"
|
||||
)->execute([$username, $hash, $display_name, $family['id']]);
|
||||
|
||||
$user_id = (int)$meta_pdo->lastInsertId();
|
||||
|
||||
// 🔥 INJECTION ICI : Ajouter la personne à la base de la famille (Couleur orangée pour différencier)
|
||||
$tenant_db = $family['db_name'];
|
||||
$tenantPdo = new PDO("mysql:host=$db_host;dbname=$tenant_db;charset=utf8mb4", $db_user, $db_pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||
$stmtPerson = $tenantPdo->prepare("INSERT INTO pf_people (name, user_id, role, color, is_active) VALUES (?, ?, 'parent', '#f59e0b', 1)");
|
||||
$stmtPerson->execute([$display_name, $user_id]);
|
||||
|
||||
$_SESSION['user'] = [
|
||||
'id' => (int)$meta_pdo->lastInsertId(),
|
||||
'id' => $user_id,
|
||||
'username' => $username,
|
||||
'display_name' => $display_name,
|
||||
'family_id' => (int)$family['id'],
|
||||
];
|
||||
$_SESSION['family_db'] = $family['db_name'];
|
||||
$_SESSION['family_db'] = $tenant_db;
|
||||
session_regenerate_id(true);
|
||||
header('Location: /index.php');
|
||||
exit;
|
||||
@@ -129,6 +178,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
)->execute([$username, $hash, $display_name, $family_id]);
|
||||
$user_id = (int)$meta_pdo->lastInsertId();
|
||||
|
||||
$tenantPdo = new PDO("mysql:host=$db_host;dbname=$db_name;charset=utf8mb4", $db_user, $db_pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||
$stmtPerson = $tenantPdo->prepare("INSERT INTO pf_people (name, user_id, role, color, is_active) VALUES (?, ?, 'parent', '#0891b2', 1)");
|
||||
$stmtPerson->execute([$display_name, $user_id]);
|
||||
|
||||
generateCalendarWeeks($tenantPdo);
|
||||
|
||||
$meta_pdo->commit();
|
||||
|
||||
$_SESSION['user'] = [
|
||||
@@ -169,7 +224,6 @@ require __DIR__ . '/header.php';
|
||||
<div class="pf-login-error"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Onglets -->
|
||||
<div style="display:flex;gap:8px;margin-bottom:24px">
|
||||
<button id="tab-create" onclick="switchTab('create')"
|
||||
class="pf-btn" style="flex:1">Nouvel espace</button>
|
||||
@@ -177,7 +231,6 @@ require __DIR__ . '/header.php';
|
||||
class="pf-btn btn-secondary" style="flex:1">Rejoindre</button>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire commun -->
|
||||
<form method="post" action="/register.php" id="reg-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
||||
<input type="hidden" name="action" id="reg-action" value="create">
|
||||
@@ -208,7 +261,6 @@ require __DIR__ . '/header.php';
|
||||
placeholder="••••••" autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
<!-- Champs spécifiques : Nouvel espace -->
|
||||
<div id="section-create">
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label">Nom de la famille / foyer</label>
|
||||
@@ -218,7 +270,6 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Champs spécifiques : Rejoindre -->
|
||||
<div id="section-join" style="display:none">
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label">Code d'invitation</label>
|
||||
|
||||
+260
-23
@@ -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,
|
||||
@@ -13,9 +21,13 @@ CREATE TABLE IF NOT EXISTS pf_users (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ─── Personnes ────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_people (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL
|
||||
CREATE TABLE `pf_people` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`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;
|
||||
|
||||
-- ─── Calendrier familial ──────────────────────────────────────────────────────
|
||||
@@ -91,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,
|
||||
@@ -103,24 +115,26 @@ CREATE TABLE IF NOT EXISTS pf_expenses (
|
||||
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,
|
||||
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
|
||||
@@ -147,9 +161,23 @@ 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,
|
||||
`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(),
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ─── Notes / Memo ─────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_notes (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
note_type VARCHAR(100) NOT NULL,
|
||||
@@ -158,6 +186,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,
|
||||
@@ -225,16 +276,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;
|
||||
|
||||
-- Personnes (IDs fixes correspondant aux constantes dans config.php)
|
||||
INSERT IGNORE INTO pf_people (id, name) VALUES (2, 'Alex');
|
||||
INSERT IGNORE INTO pf_people (id, name) VALUES (3, 'Laia');
|
||||
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 +452,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;
|
||||
+164
-114
@@ -31,21 +31,15 @@ CREATE TABLE IF NOT EXISTS user_calendar_integrations (
|
||||
// ─── Actions ──────────────────────────────────────────────────────────────────
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!verify_csrf($_POST['csrf_token'] ?? null)) {
|
||||
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => 'Session invalide (CSRF). Rechargez la page.']);
|
||||
exit;
|
||||
}
|
||||
$error = "Session invalide (CSRF). Rechargez la page.";
|
||||
} else {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'update_family_info') {
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
|
||||
$foyer = $pdo->query("SELECT currency, zone_scolaire FROM pf_foyer_settings WHERE id = 1")->fetch();
|
||||
$currency = isset($_POST['currency']) ? trim($_POST['currency']) : ($foyer['currency'] ?? '€');
|
||||
$zone = isset($_POST['zone_scolaire']) ? trim($_POST['zone_scolaire']) : ($foyer['zone_scolaire'] ?? 'C');
|
||||
$stmtSave = $pdo->prepare("UPDATE pf_foyer_settings SET currency = ?, zone_scolaire = ? WHERE id = 1");
|
||||
$stmtSave->execute([$currency, $zone]);
|
||||
$success = "Paramètres du foyer mis à jour avec succès.";
|
||||
}
|
||||
|
||||
if ($action === 'set_modules' && $family_id) {
|
||||
$all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo', 'liste', 'calendar_ios', 'printvault', 'planka'];
|
||||
$enabled = array_values(array_filter($all, fn($m) => isset($_POST['mod_' . $m])));
|
||||
@@ -55,7 +49,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$meta_pdo->prepare("UPDATE families SET enabled_modules = ? WHERE id = ?")
|
||||
->execute([json_encode($enabled), $family_id]);
|
||||
$_SESSION['enabled_modules'] = $enabled;
|
||||
$success = "Modules mis à jour.";
|
||||
$success = "Modules mis à jour avec succès.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,19 +59,28 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$_SESSION['app_lang'] = $lang;
|
||||
$meta_pdo->prepare("UPDATE users SET lang = ? WHERE id = ?")
|
||||
->execute([$lang, $user_id]);
|
||||
$success = "Language updated.";
|
||||
$success = "Langue mise à jour.";
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'update_profile') {
|
||||
$display_name = trim($_POST['display_name'] ?? '');
|
||||
$color = trim($_POST['color'] ?? '#0891b2');
|
||||
|
||||
if (!$display_name) {
|
||||
$error = "Le prénom ne peut pas être vide.";
|
||||
} else {
|
||||
$meta_pdo->prepare("UPDATE users SET display_name = ? WHERE id = ?")
|
||||
->execute([$display_name, $user_id]);
|
||||
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
if (isset($pdo)) {
|
||||
$stmtPeopleColor = $pdo->prepare("UPDATE pf_people SET color = ? WHERE user_id = ?");
|
||||
$stmtPeopleColor->execute([$color, $user_id]);
|
||||
}
|
||||
|
||||
$_SESSION['user']['display_name'] = $display_name;
|
||||
$success = "Profil mis à jour.";
|
||||
$success = "Profil mis à jour avec succès.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +132,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$error = "Format non supporté (jpg, png, webp).";
|
||||
} else {
|
||||
$dest = $upload_dir . 'home_bg_' . $family_id . '.' . $ext;
|
||||
// Supprimer anciens fichiers de ce family
|
||||
foreach (glob($upload_dir . 'home_bg_' . $family_id . '.*') as $old) @unlink($old);
|
||||
if (move_uploaded_file($file['tmp_name'], $dest)) {
|
||||
$success = "Image d'accueil mise à jour.";
|
||||
@@ -156,9 +158,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$pdo->prepare(
|
||||
"INSERT INTO pf_notes (note_type, reference_id, content) VALUES ('grocery_settings','history_max',?) ON DUPLICATE KEY UPDATE content=VALUES(content)"
|
||||
)->execute([(string) $n]);
|
||||
$success = tr('groceries_settings_saved');
|
||||
$success = tr('groceries_settings_saved') ?: 'Paramètres des courses enregistrés.';
|
||||
} catch (\Throwable $e) {
|
||||
$error = tr('groceries_settings_error') . ' ' . $e->getMessage();
|
||||
$error = (tr('groceries_settings_error') ?: 'Erreur') . ' : ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,7 +183,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
")->execute([$user_id, $username, $encrypted, $resolvedUrl]);
|
||||
$msg = "Connexion calendrier iOS enregistrée.";
|
||||
if (rtrim($resolvedUrl, '/') !== rtrim($calendarUrl, '/')) {
|
||||
$msg .= " URL du calendrier détectée automatiquement (tu avais mis la racine iCloud).";
|
||||
$msg .= " URL du calendrier détectée automatiquement.";
|
||||
}
|
||||
$success = $msg;
|
||||
} catch (\Throwable $e) {
|
||||
@@ -209,7 +211,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
$success = "Connexion iCloud CalDAV valide (HTTP $code).";
|
||||
} else {
|
||||
$error = "Test connexion échoué (HTTP $code). Vérifie identifiant Apple, mot de passe d’app (16 caractères) et que le compte iCloud a le Calendrier activé.";
|
||||
$error = "Test connexion échoué (HTTP $code). Vérifiez vos identifiants.";
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$error = "Test connexion impossible: " . $e->getMessage();
|
||||
@@ -221,16 +223,63 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$meta_pdo->prepare("DELETE FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'")->execute([$user_id]);
|
||||
$success = "Connexion iOS supprimée.";
|
||||
}
|
||||
|
||||
// --- GESTION DES PROFILS RATTACHÉS (Enfants, Nounous, etc.) ---
|
||||
if ($action === 'add_attached_profile') {
|
||||
$name = trim($_POST['profile_name'] ?? '');
|
||||
$role = trim($_POST['profile_role'] ?? 'child');
|
||||
|
||||
if ($name) {
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
if (isset($pdo)) {
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_people (name, role) VALUES (?, ?)");
|
||||
$stmt->execute([$name, $role]);
|
||||
$success = "Profil de {$name} ajouté avec succès.";
|
||||
}
|
||||
} else {
|
||||
$error = "Le prénom est obligatoire.";
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'delete_attached_profile') {
|
||||
$profile_id = (int)($_POST['profile_id'] ?? 0);
|
||||
if ($profile_id > 0) {
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
if (isset($pdo)) {
|
||||
// On protège les vrais utilisateurs en s'assurant que user_id est NULL
|
||||
$stmt = $pdo->prepare("DELETE FROM pf_people WHERE id = ? AND user_id IS NULL");
|
||||
$stmt->execute([$profile_id]);
|
||||
$success = "Profil supprimé.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Chargement données ───────────────────────────────────────────────────────
|
||||
$user = $meta_pdo->prepare("SELECT * FROM users WHERE id = ?");
|
||||
$user->execute([$user_id]);
|
||||
$user = $user->fetch();
|
||||
|
||||
if ($family_id) {
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
}
|
||||
|
||||
$stmtUser = $meta_pdo->prepare("SELECT * FROM users WHERE id = ?");
|
||||
$stmtUser->execute([$user_id]);
|
||||
$user = $stmtUser->fetch();
|
||||
|
||||
$user_color = '#0891b2';
|
||||
if ($family_id && isset($pdo)) {
|
||||
$stmtColorFetch = $pdo->prepare("SELECT color FROM pf_people WHERE user_id = ?");
|
||||
$stmtColorFetch->execute([$user_id]);
|
||||
$fetched_color = $stmtColorFetch->fetchColumn();
|
||||
if ($fetched_color) {
|
||||
$user_color = $fetched_color;
|
||||
}
|
||||
}
|
||||
|
||||
$family = null;
|
||||
$members = [];
|
||||
$attached_profiles = [];
|
||||
|
||||
if ($family_id) {
|
||||
$fam = $meta_pdo->prepare("SELECT * FROM families WHERE id = ?");
|
||||
$fam->execute([$family_id]);
|
||||
@@ -239,6 +288,12 @@ if ($family_id) {
|
||||
$mem = $meta_pdo->prepare("SELECT display_name, username, created_at, is_admin FROM users WHERE family_id = ? ORDER BY id");
|
||||
$mem->execute([$family_id]);
|
||||
$members = $mem->fetchAll();
|
||||
|
||||
// Récupération des profils rattachés (ceux qui n'ont pas de compte de connexion, donc user_id IS NULL)
|
||||
if (isset($pdo)) {
|
||||
$stmtAttached = $pdo->query("SELECT id, name, role FROM pf_people WHERE user_id IS NULL ORDER BY role ASC, name ASC");
|
||||
$attached_profiles = $stmtAttached->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
}
|
||||
|
||||
$calendarIntegration = $meta_pdo->prepare("SELECT username, calendar_url, status, last_sync_at FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'");
|
||||
@@ -246,18 +301,13 @@ $calendarIntegration->execute([$user_id]);
|
||||
$calendarIntegration = $calendarIntegration->fetch();
|
||||
|
||||
$groceryHistoryMaxSetting = 20;
|
||||
if ($family_id) {
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
if (isset($pdo)) {
|
||||
if ($family_id && isset($pdo)) {
|
||||
try {
|
||||
$gv = $pdo->query("SELECT content FROM pf_notes WHERE note_type='grocery_settings' AND reference_id='history_max'")->fetchColumn();
|
||||
if ($gv !== false && $gv !== null && $gv !== '') {
|
||||
$groceryHistoryMaxSetting = max(1, min(50, (int) $gv));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {}
|
||||
}
|
||||
|
||||
$pageTitle = "Paramètres — HouseHub";
|
||||
@@ -269,14 +319,6 @@ require __DIR__ . '/header.php';
|
||||
|
||||
<h1 class="pf-settings-title">⚙️ Paramètres</h1>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="pf-alert pf-alert--success">✓ <?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($error): ?>
|
||||
<div class="pf-alert pf-alert--error">✗ <?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- ── Langue ─────────────────────────────────────────────────────────── -->
|
||||
<section class="pf-panel-card">
|
||||
<h2 class="pf-card-h2">🌐 Langue / Language</h2>
|
||||
<form method="post" class="pf-lang-form">
|
||||
@@ -303,7 +345,6 @@ require __DIR__ . '/header.php';
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- ── Modules ────────────────────────────────────────────────────────── -->
|
||||
<?php if ($family_id): ?>
|
||||
<section class="pf-panel-card">
|
||||
<h2 class="pf-card-h2 pf-card-h2--tight">🧩 Modules actifs</h2>
|
||||
@@ -336,7 +377,7 @@ require __DIR__ . '/header.php';
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<button type="submit" class="pf-btn">Enregistrer</button>
|
||||
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
|
||||
</form>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
@@ -358,20 +399,12 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
<button type="submit" class="pf-btn"><?= htmlspecialchars(tr('btn_save')) ?></button>
|
||||
</form>
|
||||
<p class="pf-muted-note" style="margin-top:1rem">
|
||||
<a href="/liste.php" style="color:var(--primary);font-weight:600;"><?= htmlspecialchars(tr('mod_liste_name')) ?></a>
|
||||
</p>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="pf-panel-card">
|
||||
<h2 class="pf-card-h2 pf-card-h2--tight">📱 Intégration Calendrier iOS (CalDAV)</h2>
|
||||
<p class="pf-muted-note">Configurez ici votre calendrier iCloud pour synchroniser les événements créés dans HouseHub.</p>
|
||||
<p class="pf-muted-note" style="margin-top:8px;">
|
||||
Pour voir et gérer les événements : ouvrez le module
|
||||
<a href="/calendar-ios.php" style="color:var(--primary);font-weight:600;">Calendrier iOS</a>
|
||||
(menu du haut ou burger « Calendrier iOS », ou carte sur l’accueil). Le module doit être coché dans « Modules actifs » ci-dessus.
|
||||
</p>
|
||||
<form method="post" class="pf-stack-md">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
||||
<input type="hidden" name="action" value="calendar_ios_save">
|
||||
@@ -388,7 +421,7 @@ require __DIR__ . '/header.php';
|
||||
<input type="url" name="icloud_calendar_url" class="pf-input" value="<?= htmlspecialchars($calendarIntegration['calendar_url'] ?? '') ?>" placeholder="https://caldav.icloud.com/..." required>
|
||||
</div>
|
||||
<div class="pf-flex-gap-8">
|
||||
<button type="submit" class="pf-btn">Enregistrer</button>
|
||||
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -404,16 +437,8 @@ require __DIR__ . '/header.php';
|
||||
<button type="submit" class="pf-btn btn-secondary">Déconnecter</button>
|
||||
</form>
|
||||
</div>
|
||||
<p class="pf-muted-note" style="margin-top:10px;">
|
||||
Après enregistrement, utilisez « Tester la connexion » : un message vert confirme que l’URL CalDAV répond avec tes identifiants.
|
||||
Sur la page Calendrier iOS, le bouton « Synchroniser » envoie les événements vers iCloud et récupère ceux créés sur l’iPhone.
|
||||
</p>
|
||||
<?php if (!empty($calendarIntegration['last_sync_at'])): ?>
|
||||
<p class="pf-muted-note">Dernière synchro: <?= htmlspecialchars($calendarIntegration['last_sync_at']) ?></p>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<!-- ── Fond page d'accueil ───────────────────────────────────────────── -->
|
||||
<?php if ($family_id):
|
||||
$bg_file = null;
|
||||
foreach (glob('/uploads/home_bg_' . $family_id . '.*') as $f) { $bg_file = $f; break; }
|
||||
@@ -451,58 +476,6 @@ require __DIR__ . '/header.php';
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<!-- ── Famille ──────────────────────────────────────────────────────────── -->
|
||||
<?php
|
||||
$enabledMods = $_SESSION['enabled_modules'] ?? [];
|
||||
|
||||
// On cible précisément qui a besoin de quoi
|
||||
$showCurrency = count(array_intersect(['budget', 'holidays', 'gifts', 'garage'], $enabledMods)) > 0;
|
||||
$showZone = in_array('calendar', $enabledMods);
|
||||
|
||||
// Si au moins un des champs doit être affiché, on rend la section visible
|
||||
if ($family_id && ($showCurrency || $showZone)):
|
||||
$currentCurrency = defined('CURRENCY') ? CURRENCY : '€';
|
||||
$currentZone = defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : 'C';
|
||||
?>
|
||||
<section class="pf-panel-card">
|
||||
<h2 class="pf-card-h2">👪 Configuration du foyer</h2>
|
||||
<p class="pf-muted-note">Configurez les indicateurs partagés par votre foyer.</p>
|
||||
|
||||
<form method="post" class="pf-stack-md">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
||||
<input type="hidden" name="action" value="update_family_info">
|
||||
|
||||
<div class="form-row">
|
||||
|
||||
<?php if ($showCurrency): ?>
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label">Devise monétaire</label>
|
||||
<input type="text" name="currency" class="pf-input" value="<?= htmlspecialchars($currentCurrency) ?>" placeholder="ex: €, $, CHF" required maxlength="10">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($showZone): ?>
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label">Zone de vacances scolaires (France)</label>
|
||||
<select name="zone_scolaire" class="pf-input" style="background:var(--bg-panel);">
|
||||
<option value="A" <?= $currentZone === 'A' ? 'selected' : '' ?>>Zone A</option>
|
||||
<option value="B" <?= $currentZone === 'B' ? 'selected' : '' ?>>Zone B</option>
|
||||
<option value="C" <?= $currentZone === 'C' ? 'selected' : '' ?>>Zone C</option>
|
||||
<option value="Autre" <?= $currentZone === 'Autre' ? 'selected' : '' ?>>Hors France / Autre</option>
|
||||
</select>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<button type="submit" class="pf-btn">Enregistrer les paramètres du foyer</button>
|
||||
</form>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<!-- ── Profil ──────────────────────────────────────────────────────────── -->
|
||||
<section class="pf-panel-card">
|
||||
<h2 class="pf-card-h2">👤 Mon profil</h2>
|
||||
<form method="post">
|
||||
@@ -516,11 +489,19 @@ require __DIR__ . '/header.php';
|
||||
<label class="pf-label">Prénom / Pseudo affiché</label>
|
||||
<input type="text" name="display_name" class="pf-input" value="<?= htmlspecialchars($user['display_name']) ?>" required>
|
||||
</div>
|
||||
<button type="submit" class="pf-btn">Enregistrer</button>
|
||||
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label" for="profile_color">Couleur thématique de mon profil</label>
|
||||
<div class="pf-color-picker-group">
|
||||
<input type="color" name="color" id="profile_color" class="pf-input-color" value="<?= htmlspecialchars($user_color) ?>">
|
||||
<span class="pf-muted-note">Cette couleur sera utilisée pour vos grilles de budget, calendriers et indicateurs personnels.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- ── Mot de passe ────────────────────────────────────────────────────── -->
|
||||
<section class="pf-panel-card">
|
||||
<h2 class="pf-card-h2">🔒 Changer le mot de passe</h2>
|
||||
<form method="post">
|
||||
@@ -543,7 +524,6 @@ require __DIR__ . '/header.php';
|
||||
</section>
|
||||
|
||||
<?php if ($family): ?>
|
||||
<!-- ── Espace familial ─────────────────────────────────────────────────── -->
|
||||
<section class="pf-panel-card">
|
||||
<h2 class="pf-card-h2">🏠 Mon espace familial</h2>
|
||||
|
||||
@@ -559,7 +539,6 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Code d'invitation -->
|
||||
<div class="pf-dashed-panel">
|
||||
<p class="pf-muted-note pf-muted-note--tight">Code d'invitation — partagez-le pour inviter quelqu'un</p>
|
||||
<div class="pf-invite-row">
|
||||
@@ -582,7 +561,6 @@ require __DIR__ . '/header.php';
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Membres -->
|
||||
<h3 class="pf-members-heading">Membres (<?= count($members) ?>)</h3>
|
||||
<div class="pf-stack-sm">
|
||||
<?php foreach ($members as $m): ?>
|
||||
@@ -598,10 +576,61 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<h3 class="pf-members-heading" style="margin-top: 2rem;">🧸 Profils rattachés</h3>
|
||||
<p class="pf-muted-note pf-muted-note--tight" style="margin-bottom: 1rem;">Membres gérés par le foyer (Enfants, Nounous, Proches) n'ayant pas d'accès direct.</p>
|
||||
|
||||
<div class="pf-stack-sm" style="margin-bottom: 1.5rem;">
|
||||
<?php foreach ($attached_profiles as $p):
|
||||
$roleBadge = 'Inconnu';
|
||||
if (in_array($p['role'], ['child', 'enfant'])) $roleBadge = '👶 Enfant';
|
||||
elseif ($p['role'] === 'helper') $roleBadge = '💼 Intervenant';
|
||||
elseif ($p['role'] === 'relative') $roleBadge = '👵 Proche';
|
||||
?>
|
||||
<div class="pf-member-card" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<strong><?= htmlspecialchars($p['name']) ?></strong>
|
||||
<span class="pf-muted-inline"><?= $roleBadge ?></span>
|
||||
</div>
|
||||
<form method="post" style="margin:0;">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
||||
<input type="hidden" name="action" value="delete_attached_profile">
|
||||
<input type="hidden" name="profile_id" value="<?= $p['id'] ?>">
|
||||
<button type="submit" class="btn-icon-action delete" title="Supprimer" style="background: none; border: none; cursor: pointer; font-size: 1.2rem; padding: 4px;" onclick="return confirm('Supprimer définitivement ce profil ?')">🗑️</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if (empty($attached_profiles)): ?>
|
||||
<div class="pf-muted-note" style="padding: 10px; text-align: center; border: 1px dashed var(--border-light); border-radius: 8px;">Aucun profil rattaché.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<form method="post" style="display: flex; gap: 8px; background: var(--bg-soft); padding: 12px; border-radius: 8px; border: 1px dashed var(--border-light); align-items: flex-end;">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
||||
<input type="hidden" name="action" value="add_attached_profile">
|
||||
|
||||
<div class="pf-form-group" style="margin-bottom: 0; flex: 2;">
|
||||
<label class="pf-label" style="font-size: 0.8rem; margin-bottom: 4px;">Prénom</label>
|
||||
<input type="text" name="profile_name" class="pf-input" placeholder="Ex: Pol, Carole..." required>
|
||||
</div>
|
||||
|
||||
<div class="pf-form-group" style="margin-bottom: 0; flex: 1.5;">
|
||||
<label class="pf-label" style="font-size: 0.8rem; margin-bottom: 4px;">Rôle</label>
|
||||
<select name="profile_role" class="pf-input" required>
|
||||
<option value="child">👶 Enfant</option>
|
||||
<option value="helper">💼 Intervenant</option>
|
||||
<option value="relative">👵 Proche</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="pf-btn pf-shrink-0" style="padding: 8px 16px;">Ajouter</button>
|
||||
</form>
|
||||
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($_SESSION['user']['is_admin']): ?>
|
||||
<?php if (!empty($_SESSION['user']['is_admin'])): ?>
|
||||
<div class="pf-settings-admin-link">
|
||||
<a href="/admin/">→ Panneau d'administration</a>
|
||||
</div>
|
||||
@@ -610,6 +639,7 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// --- INJECTION CSRF GLOBALE ---
|
||||
document.querySelectorAll('form[method="post"]').forEach((form) => {
|
||||
if (!form.querySelector('input[name="csrf_token"]')) {
|
||||
const input = document.createElement('input');
|
||||
@@ -620,6 +650,7 @@ document.querySelectorAll('form[method="post"]').forEach((form) => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- COPIE DU CODE D'INVITATION ---
|
||||
function copyCode() {
|
||||
const code = document.getElementById('invite-code').textContent.trim();
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
@@ -628,6 +659,25 @@ function copyCode() {
|
||||
setTimeout(() => msg.classList.remove('is-visible'), 2000);
|
||||
});
|
||||
}
|
||||
|
||||
// --- GESTION DES NOTIFICATIONS TOAST (Remplaçant l'ancien bandeau vert) ---
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
<?php if ($success): ?>
|
||||
if (typeof showToast === 'function') {
|
||||
showToast(<?= json_encode($success) ?>, 'success');
|
||||
} else {
|
||||
alert(<?= json_encode($success) ?>);
|
||||
}
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($error): ?>
|
||||
if (typeof showToast === 'function') {
|
||||
showToast(<?= json_encode($error) ?>, 'error');
|
||||
} else {
|
||||
alert(<?= json_encode($error) ?>);
|
||||
}
|
||||
<?php endif; ?>
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require __DIR__ . '/footer.php'; ?>
|
||||
Reference in New Issue
Block a user