refacto parent Calendrier budget et couleur profil.
Deploy HouseHub / deploy (push) Successful in 2s
Deploy HouseHub / deploy (push) Successful in 2s
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -24,10 +24,13 @@ CREATE TABLE IF NOT EXISTS pf_foyer_settings (
|
||||
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
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
CREATE TABLE `pf_people` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(50) NOT NULL,
|
||||
`user_id` int(11) DEFAULT NULL,
|
||||
`role` varchar(50) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
||||
|
||||
-- ─── Calendrier familial ──────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_events (
|
||||
@@ -243,9 +246,6 @@ SET FOREIGN_KEY_CHECKS = 1;
|
||||
INSERT IGNORE INTO pf_users (id, username, password_hash, display_name)
|
||||
VALUES (1, 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin');
|
||||
|
||||
-- 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');
|
||||
|
||||
-- ─── Garage Manager ───────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_vehicles (
|
||||
|
||||
+24
-10
@@ -8,6 +8,12 @@ require_login();
|
||||
require __DIR__ . '/includes/db.php';
|
||||
require_once __DIR__ . '/includes/i18n.php';
|
||||
|
||||
$stmtPeople = $pdo->query("SELECT id, name, user_id, role FROM pf_people ORDER BY id ASC");
|
||||
$familyPeople = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
|
||||
$parents = array_values(array_filter($familyPeople, function($p) {
|
||||
return strtolower($p['role'] ?? '') === 'parent';
|
||||
}));
|
||||
|
||||
$pageTitle = tr('fc_page_title');
|
||||
$activePage = "family-calendar";
|
||||
$mainClass = "pf-family-calendar";
|
||||
@@ -70,8 +76,12 @@ 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 ($familyPeople as $person): ?>
|
||||
<option value="<?= htmlspecialchars($person['id']) ?>">
|
||||
<?= htmlspecialchars($person['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="pf-form-group">
|
||||
@@ -169,20 +179,24 @@ require __DIR__ . '/header.php';
|
||||
<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>
|
||||
<th colspan="6" class="col-alex header-group"><?= htmlspecialchars(strtoupper($parents[0]['name'] ?? 'PARENT 1')) ?></th>
|
||||
<th colspan="6" class="col-laia header-group"><?= htmlspecialchars(strtoupper($parents[1]['name'] ?? 'PARENT 2')) ?></th>
|
||||
</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>
|
||||
</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';
|
||||
?>
|
||||
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-av"><?= tr('fc_col_av') ?></th>
|
||||
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-use"><?= tr('fc_col_use') ?></th>
|
||||
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-av"><?= tr('fc_col_av') ?></th>
|
||||
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-use"><?= tr('fc_col_use') ?></th>
|
||||
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-av"><?= tr('fc_col_av') ?></th>
|
||||
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-use"><?= tr('fc_col_use') ?></th>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="planningBody"></tbody>
|
||||
|
||||
+257
-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,207 @@ 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);
|
||||
}
|
||||
[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"; ?>'
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
<?php
|
||||
if (file_exists(__DIR__ . '/config.php')) {
|
||||
require_once __DIR__ . '/config.php';
|
||||
}
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
+125
-20
@@ -1,46 +1,151 @@
|
||||
<?php
|
||||
// Script de migration pour mettre à jour TOUTES les bases de données familiales
|
||||
// Script de migration : Refonte globale Multi-Tenant (pf_people & Budget)
|
||||
require_once __DIR__ . '/includes/meta_db.php';
|
||||
|
||||
echo "<h1>🚀 Début de la migration...</h1>";
|
||||
echo "<h1>🚀 Début de la migration Globale ...</h1>";
|
||||
|
||||
// 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 != ''");
|
||||
$stmt = $meta_pdo->query("SELECT id, 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) ... ";
|
||||
$family_id = $f['id'];
|
||||
echo "<h3>Mise à jour de <strong>{$f['name']}</strong> ($db_name)</h3><ul>";
|
||||
|
||||
try {
|
||||
$fam_pdo = new PDO("mysql:host=$host;dbname=$db_name;charset=utf8mb4", $user, $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. GESTION DE PF_PEOPLE (user_id, role, color)
|
||||
// ==========================================
|
||||
echo "<li><strong>Table pf_people :</strong> ";
|
||||
|
||||
INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '€', 'C');
|
||||
";
|
||||
// user_id
|
||||
try {
|
||||
$fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN user_id INT NULL DEFAULT NULL");
|
||||
echo "<span style='color:green'>user_id OK</span> - ";
|
||||
} catch (\PDOException $e) {
|
||||
if ($e->getCode() == '42S21' || strpos($e->getMessage(), '1060') !== false) {
|
||||
echo "<span style='color:gray'>user_id déjà présent</span> - ";
|
||||
} else { throw $e; }
|
||||
}
|
||||
|
||||
$fam_pdo->exec($sql);
|
||||
echo "<span style='color:green'>✅ OK</span><br>";
|
||||
// Auto-mapping
|
||||
$stmtUsers = $meta_pdo->prepare("SELECT id, username FROM users WHERE family_id = ?");
|
||||
$stmtUsers->execute([$family_id]);
|
||||
$users = $stmtUsers->fetchAll();
|
||||
$mappedCount = 0;
|
||||
foreach ($users as $u) {
|
||||
$stmtUpdate = $fam_pdo->prepare("UPDATE pf_people SET user_id = ? WHERE LOWER(name) = LOWER(?) AND user_id IS NULL");
|
||||
$stmtUpdate->execute([$u['id'], $u['username']]);
|
||||
$mappedCount += $stmtUpdate->rowCount();
|
||||
}
|
||||
echo "<span style='color:blue'>$mappedCount profils liés</span> - ";
|
||||
|
||||
// role
|
||||
try {
|
||||
$fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN role VARCHAR(50) DEFAULT NULL");
|
||||
echo "<span style='color:green'>role OK</span> - ";
|
||||
} catch (\PDOException $e) {
|
||||
if ($e->getCode() == '42S21' || strpos($e->getMessage(), '1060') !== false) {
|
||||
echo "<span style='color:gray'>role déjà présent</span> - ";
|
||||
} else { throw $e; }
|
||||
}
|
||||
|
||||
// 🟢 NOUVEAU : Colonne color
|
||||
try {
|
||||
$fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN color VARCHAR(7) DEFAULT '#0891b2'");
|
||||
echo "<span style='color:green'>colonne 'color' ajoutée</span> - ";
|
||||
} catch (\PDOException $e) {
|
||||
if ($e->getCode() == '42S21' || strpos($e->getMessage(), '1060') !== false) {
|
||||
echo "<span style='color:gray'>colonne 'color' déjà présente</span> - ";
|
||||
} else { throw $e; }
|
||||
}
|
||||
|
||||
// Configuration des valeurs par défaut et des rôles
|
||||
$fam_pdo->exec("
|
||||
UPDATE pf_people SET role = 'parent' WHERE user_id IS NOT NULL;
|
||||
UPDATE pf_people SET role = 'nounou' WHERE LOWER(name) = 'carole';
|
||||
");
|
||||
|
||||
// On donne des couleurs distinctes par défaut aux deux parents (P1 = Cyan, P2 = Orange)
|
||||
$stmtP = $fam_pdo->query("SELECT id FROM pf_people WHERE role = 'parent' ORDER BY id ASC");
|
||||
$pIds = $stmtP->fetchAll(PDO::FETCH_COLUMN);
|
||||
if (isset($pIds[0])) {
|
||||
$fam_pdo->exec("UPDATE pf_people SET color = '#0891b2' WHERE id = " . (int)$pIds[0]);
|
||||
}
|
||||
if (isset($pIds[1])) {
|
||||
$fam_pdo->exec("UPDATE pf_people SET color = '#f59e0b' WHERE id = " . (int)$pIds[1]);
|
||||
}
|
||||
|
||||
echo "<span style='color:blue'>Rôles et Couleurs initialisés.</span></li>";
|
||||
|
||||
// ==========================================
|
||||
// 2. GESTION DU BUDGET (pf_alloc_values)
|
||||
// ==========================================
|
||||
echo "<li><strong>Table pf_alloc_values :</strong> ";
|
||||
|
||||
// Vérification et renommage de amount_alex
|
||||
$checkAlex = $fam_pdo->query("SHOW COLUMNS FROM pf_alloc_values LIKE 'amount_alex'")->rowCount();
|
||||
if ($checkAlex > 0) {
|
||||
// Utilisation de CHANGE pour compatibilité maximale avec les anciennes versions MySQL
|
||||
$fam_pdo->exec("ALTER TABLE pf_alloc_values CHANGE amount_alex amount_p1 FLOAT DEFAULT 0");
|
||||
echo "<span style='color:green'>amount_alex -> amount_p1</span> - ";
|
||||
} else {
|
||||
echo "<span style='color:gray'>amount_p1 OK</span> - ";
|
||||
}
|
||||
|
||||
// Vérification et renommage de amount_laia
|
||||
$checkLaia = $fam_pdo->query("SHOW COLUMNS FROM pf_alloc_values LIKE 'amount_laia'")->rowCount();
|
||||
if ($checkLaia > 0) {
|
||||
$fam_pdo->exec("ALTER TABLE pf_alloc_values CHANGE amount_laia amount_p2 FLOAT DEFAULT 0");
|
||||
echo "<span style='color:green'>amount_laia -> amount_p2</span></li>";
|
||||
} else {
|
||||
echo "<span style='color:gray'>amount_p2 OK</span></li>";
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 3. GESTION DU BUDGET (pf_alloc_categories)
|
||||
// ==========================================
|
||||
echo "<li><strong>Table pf_alloc_categories :</strong> ";
|
||||
$stmtCats1 = $fam_pdo->exec("UPDATE pf_alloc_categories SET name = 'Eco P1' WHERE name LIKE 'Eco Alex%'");
|
||||
$stmtCats2 = $fam_pdo->exec("UPDATE pf_alloc_categories SET name = 'Eco P2' WHERE name LIKE 'Eco Laia%'");
|
||||
echo "<span style='color:green'>Catégories 'Eco' génériques mises à jour (" . ($stmtCats1 + $stmtCats2) . " lignes).</span></li>";
|
||||
|
||||
// ==========================================
|
||||
// 4. GESTION DU BUDGET (pf_salary_config)
|
||||
// ==========================================
|
||||
echo "<li><strong>Table pf_salary_config :</strong> ";
|
||||
// On récupère les vrais prénoms pour les remplacer dans les configs de salaires
|
||||
$stmtParents = $fam_pdo->query("SELECT name FROM pf_people WHERE role = 'parent' ORDER BY id ASC");
|
||||
$parents = $stmtParents->fetchAll();
|
||||
|
||||
if (count($parents) >= 2) {
|
||||
$p1_name = $parents[0]['name'];
|
||||
$p2_name = $parents[1]['name'];
|
||||
|
||||
$stmtSal1 = $fam_pdo->prepare("UPDATE pf_salary_config SET person = ? WHERE person = 'Alex'");
|
||||
$stmtSal1->execute([$p1_name]);
|
||||
|
||||
$stmtSal2 = $fam_pdo->prepare("UPDATE pf_salary_config SET person = ? WHERE person = 'Laia'");
|
||||
$stmtSal2->execute([$p2_name]);
|
||||
|
||||
echo "<span style='color:green'>Salaires mappés vers $p1_name et $p2_name.</span></li>";
|
||||
} else {
|
||||
echo "<span style='color:orange'>Pas assez de parents trouvés pour mapper les salaires.</span></li>";
|
||||
}
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
echo "<span style='color:red'>❌ Erreur : " . $e->getMessage() . "</span><br>";
|
||||
echo "<li style='color:red'>❌ Erreur : " . $e->getMessage() . "</li>";
|
||||
}
|
||||
echo "</ul>";
|
||||
}
|
||||
|
||||
echo "<h2>🎉 Migration terminée ! Tu peux supprimer ce fichier.</h2>";
|
||||
echo "<h2>🎉 Migration terminée avec succès !</h2>";
|
||||
?>
|
||||
+273
-69
@@ -1,7 +1,6 @@
|
||||
/* modules/budget/budget.css */
|
||||
|
||||
/* --- 1. VARIABLES & BASE --- */
|
||||
/* Le root garde les couleurs sémantiques dont le JS/PHP a besoin localement */
|
||||
.budget-view {
|
||||
font-family:
|
||||
"Segoe UI",
|
||||
@@ -128,7 +127,7 @@
|
||||
|
||||
/* --- 3. BOUTONS SPÉCIFIQUES & ACTIONS --- */
|
||||
.pf-btn {
|
||||
border-radius: 50px; /* Surcharge le global pour le budget */
|
||||
border-radius: 50px;
|
||||
}
|
||||
.btn-icon {
|
||||
background: transparent;
|
||||
@@ -263,7 +262,6 @@ td:hover .btn-cell-delete {
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
/* Inputs modifiés */
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -346,20 +344,94 @@ input[type="number"] {
|
||||
gap: 30px;
|
||||
}
|
||||
.prev-section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
justify-content: space-between !important;
|
||||
align-items: center !important;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
width: 100% !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.prev-section-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
.prev-header-left {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
.prev-header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Configuration Chromatique Parent 1 */
|
||||
.txt-p1,
|
||||
.txt-alex {
|
||||
color: color-mix(in srgb, var(--p1-main, #0891b2) 85%, #000000) !important;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
.col-p1,
|
||||
.col-alex {
|
||||
color: color-mix(in srgb, var(--p1-main, #0891b2) 85%, #000000) !important;
|
||||
text-align: center;
|
||||
}
|
||||
th.col-p1,
|
||||
td.col-p1 {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--p1-main, #0891b2) 8%,
|
||||
#ffffff
|
||||
) !important;
|
||||
text-align: center;
|
||||
}
|
||||
input.txt-p1 {
|
||||
color: color-mix(in srgb, var(--p1-main, #0891b2) 85%, #000000) !important;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
input.txt-p1:focus {
|
||||
border-color: var(--p1-main, #0891b2) !important;
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--p1-main, #0891b2) 10%, #ffffff) !important;
|
||||
}
|
||||
|
||||
/* Configuration Chromatique Parent 2 */
|
||||
.txt-p2 {
|
||||
color: color-mix(in srgb, var(--p2-main, #f59e0b) 85%, #000000) !important;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
.col-p2 {
|
||||
color: color-mix(in srgb, var(--p2-main, #f59e0b) 85%, #000000) !important;
|
||||
text-align: center;
|
||||
}
|
||||
th.col-p2,
|
||||
td.col-p2 {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--p2-main, #f59e0b) 8%,
|
||||
#ffffff
|
||||
) !important;
|
||||
text-align: center;
|
||||
}
|
||||
input.txt-p2 {
|
||||
color: color-mix(in srgb, var(--p2-main, #f59e0b) 85%, #000000) !important;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
input.txt-p2:focus {
|
||||
border-color: var(--p2-main, #f59e0b) !important;
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--p2-main, #f59e0b) 10%, #ffffff) !important;
|
||||
}
|
||||
|
||||
.prev-input {
|
||||
width: 100%;
|
||||
@@ -385,19 +457,6 @@ input[type="number"] {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.prev-section-header,
|
||||
.view-header,
|
||||
.budget-view > div[style*="justify-content: space-between"] {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
justify-content: space-between !important;
|
||||
align-items: center !important;
|
||||
float: none !important;
|
||||
position: relative !important;
|
||||
width: 100% !important;
|
||||
gap: 15px !important;
|
||||
}
|
||||
|
||||
.prev-salary-table {
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
@@ -419,6 +478,15 @@ input[type="number"] {
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
.prev-salary-table th small {
|
||||
font-weight: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
.prev-salary-table th.th-family-savings,
|
||||
.prev-salary-table td.td-family-savings {
|
||||
background: #f0f9ff !important;
|
||||
color: #0284c7 !important;
|
||||
}
|
||||
.prev-salary-table th:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
@@ -426,6 +494,22 @@ input[type="number"] {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.prev-salary-table tbody tr:nth-child(1) td:first-child {
|
||||
text-align: left !important;
|
||||
font-weight: bold !important;
|
||||
color: var(--text-main) !important;
|
||||
border-left: 4px solid var(--p1-main, #0891b2) !important;
|
||||
}
|
||||
|
||||
/* Parent 2 (Ligne 2, ex: Laia) */
|
||||
.prev-salary-table tbody tr:nth-child(2) td:first-child {
|
||||
text-align: left !important;
|
||||
font-weight: bold !important;
|
||||
color: var(--text-main) !important;
|
||||
border-left: 4px solid var(--p2-main, #f59e0b) !important;
|
||||
}
|
||||
|
||||
.prev-timeline-wrapper {
|
||||
@@ -455,7 +539,7 @@ input[type="number"] {
|
||||
border-right: 1px solid var(--border-light);
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.col-sticky {
|
||||
@@ -490,6 +574,15 @@ input[type="number"] {
|
||||
border-left: 2px solid white;
|
||||
padding: 8px !important;
|
||||
}
|
||||
.th-month span {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.th-month .cycle-start-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: normal;
|
||||
color: #64748b;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.th-month.current {
|
||||
background: var(--primary) !important;
|
||||
}
|
||||
@@ -511,16 +604,42 @@ input[type="number"] {
|
||||
font-weight: 700;
|
||||
border-bottom: 2px solid #bfdbfe;
|
||||
}
|
||||
.txt-alex {
|
||||
color: #0891b2;
|
||||
.row-restant td.td-separator {
|
||||
border-left: 2px solid #cbd5e1;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
.txt-laia {
|
||||
color: #f59e0b;
|
||||
.row-indicative {
|
||||
background: #f8fafc;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.row-indicative .col-sticky {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.cat-name-label {
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
}
|
||||
.row-indicative .cat-name-label {
|
||||
color: #64748b;
|
||||
}
|
||||
.row-indicative .cat-name-label span {
|
||||
font-size: 0.7rem;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 4px;
|
||||
padding: 0 4px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.cat-target-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
text-align: right;
|
||||
}
|
||||
.txt-global {
|
||||
color: var(--text-muted);
|
||||
font-weight: 700;
|
||||
background: #f8fafc;
|
||||
border-left: 2px solid #e2e8f0;
|
||||
}
|
||||
.val-ok {
|
||||
color: var(--success);
|
||||
@@ -594,6 +713,9 @@ tr:hover .row-actions {
|
||||
letter-spacing: 0.05em;
|
||||
text-align: center;
|
||||
}
|
||||
.recap-header span {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.recap-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@@ -603,6 +725,7 @@ tr:hover .row-actions {
|
||||
padding: 10px 15px;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.recap-table th {
|
||||
@@ -618,30 +741,68 @@ tr:hover .row-actions {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
.col-alex {
|
||||
background-color: #ecfeff;
|
||||
color: #0891b2;
|
||||
border-left: 1px solid #cffafe;
|
||||
|
||||
.recap-th-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
th.col-alex {
|
||||
background-color: #cffafe;
|
||||
border-bottom-color: #a5f3fc;
|
||||
.recap-th-content span {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.col-laia {
|
||||
background-color: #fffbeb;
|
||||
color: #d97706;
|
||||
border-left: 1px solid #fef3c7;
|
||||
.validation-badge {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
th.col-laia {
|
||||
background-color: #fef3c7;
|
||||
border-bottom-color: #fde68a;
|
||||
|
||||
/* --- Style de base du bouton de validation (Générique) --- */
|
||||
.recap-table .pf-btn.btn-small {
|
||||
background: #ffffff !important;
|
||||
font-size: 0.7rem !important;
|
||||
padding: 2px 8px !important;
|
||||
height: auto !important;
|
||||
width: auto !important;
|
||||
font-weight: 600;
|
||||
border-radius: 9px !important;
|
||||
box-shadow: none !important;
|
||||
transform: none !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.col-global {
|
||||
background-color: #f8fafc;
|
||||
color: #475569;
|
||||
font-weight: 700;
|
||||
border-left: 2px solid #e2e8f0;
|
||||
|
||||
/* Couleurs dynamiques adaptées au Parent 1 (ex: Féfé) */
|
||||
.col-p1 .pf-btn.btn-small {
|
||||
color: var(--p1-main, #0891b2) !important;
|
||||
border: 1px solid var(--p1-main, #0891b2) !important;
|
||||
}
|
||||
.col-p1 .pf-btn.btn-small:hover {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--p1-main, #0891b2) 12%,
|
||||
#ffffff
|
||||
) !important;
|
||||
}
|
||||
|
||||
/* Couleurs dynamiques adaptées au Parent 2 (ex: Laia) */
|
||||
.col-p2 .pf-btn.btn-small {
|
||||
color: var(--p2-main, #f59e0b) !important;
|
||||
border: 1px solid var(--p2-main, #f59e0b) !important;
|
||||
}
|
||||
.col-p2 .pf-btn.btn-small:hover {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--p2-main, #f59e0b) 12%,
|
||||
#ffffff
|
||||
) !important;
|
||||
}
|
||||
|
||||
/* Grand Totaux Structuraux */
|
||||
.row-grand-total td {
|
||||
background-color: #1e293b;
|
||||
color: white;
|
||||
@@ -649,14 +810,22 @@ th.col-laia {
|
||||
border: none;
|
||||
padding: 12px 15px;
|
||||
}
|
||||
.row-grand-total .col-alex {
|
||||
background: #164e63;
|
||||
color: #67e8f9;
|
||||
.row-grand-total .col-p1 {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--p1-main, #0891b2) 65%,
|
||||
#000000
|
||||
) !important;
|
||||
color: color-mix(in srgb, var(--p1-main, #0891b2) 20%, #ffffff) !important;
|
||||
border: none;
|
||||
}
|
||||
.row-grand-total .col-laia {
|
||||
background: #78350f;
|
||||
color: #fcd34d;
|
||||
.row-grand-total .col-p2 {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--p2-main, #f59e0b) 65%,
|
||||
#000000
|
||||
) !important;
|
||||
color: color-mix(in srgb, var(--p2-main, #f59e0b) 20%, #ffffff) !important;
|
||||
border: none;
|
||||
}
|
||||
.row-grand-total .col-global {
|
||||
@@ -665,7 +834,7 @@ th.col-laia {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* --- 9. STYLES SPÉCIFIQUES AU SUIVI MENSUEL --- */
|
||||
/* --- 9. STYLES DU SUIVI MENSUEL --- */
|
||||
.cat-card {
|
||||
background: var(--bg-panel);
|
||||
border-radius: var(--radius);
|
||||
@@ -675,20 +844,17 @@ th.col-laia {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cat-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px; /* Padding interne à l'encart coloré */
|
||||
padding: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cat-card-title-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cat-card-subtitle {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
@@ -768,7 +934,52 @@ input[type="month"].pf-input {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* --- 10. EPARGNE --- */
|
||||
/* --- 10. SECTION NOTES BUDGETAIRES --- */
|
||||
.budget-notes-section {
|
||||
margin: 20px 0;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
.notes-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.notes-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
color: #1e293b;
|
||||
}
|
||||
.notes-header h3 span {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.note-save-indicator {
|
||||
font-size: 0.85rem;
|
||||
color: #10b981;
|
||||
font-weight: bold;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
#monthNoteArea {
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
border-color: #cbd5e1;
|
||||
background: #f8fafc;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.notes-footer {
|
||||
text-align: right;
|
||||
}
|
||||
.notes-footer .pf-btn {
|
||||
width: auto;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* --- 11. EPARGNE --- */
|
||||
.epargne-inline-input {
|
||||
width: 100%;
|
||||
min-width: 60px;
|
||||
@@ -793,7 +1004,7 @@ input[type="month"].pf-input {
|
||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
/* --- 11. CALCULATRICE FLOTTANTE --- */
|
||||
/* --- 12. CALCULATRICE FLOTTANTE --- */
|
||||
.pf-fab-sum {
|
||||
position: fixed;
|
||||
right: 30px;
|
||||
@@ -883,7 +1094,7 @@ body.sum-mode-active .sum-target {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
/* --- 12. RESPONSIVE MOBILE BUDGET --- */
|
||||
/* --- 13. RESPONSIVE MOBILE BUDGET --- */
|
||||
@media (max-width: 768px) {
|
||||
.budget-view {
|
||||
padding: 10px !important;
|
||||
@@ -917,11 +1128,6 @@ body.sum-mode-active .sum-target {
|
||||
.budget-view .categories-grid > .cat-card {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.budget-view
|
||||
> div[style*="justify-content: space-between"]:not(.view-header) {
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
}
|
||||
.budget-view
|
||||
> div[style*="justify-content: space-between"]:not(.view-header) {
|
||||
flex-direction: column !important;
|
||||
@@ -1021,7 +1227,6 @@ body.sum-mode-active .sum-target {
|
||||
justify-content: flex-start;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.col-sticky,
|
||||
.sticky-col {
|
||||
min-width: 120px !important;
|
||||
@@ -1059,8 +1264,7 @@ body.sum-mode-active .sum-target {
|
||||
right: auto;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
.cat-card-header {
|
||||
padding: 12px 10px; /* Plus serré sur mobile */
|
||||
padding: 12px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
<?php
|
||||
// modules/budget/views/budget_prev.php
|
||||
|
||||
// 1. Chargement dynamique des parents
|
||||
$stmtPeople = $pdo->query("SELECT id, name, user_id, role, color FROM pf_people WHERE role = 'parent' ORDER BY id ASC");
|
||||
$budgetParents = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$p1_name = $budgetParents[0]['name'] ?? 'Parent 1';
|
||||
$p2_name = $budgetParents[1]['name'] ?? 'Parent 2';
|
||||
$currentYear = date('Y');
|
||||
|
||||
// 1. Récupération Config Salaires
|
||||
// Cartographie dynamique
|
||||
$parentMapping = [];
|
||||
foreach ($budgetParents as $index => $parent) {
|
||||
$num = $index + 1;
|
||||
$parentMapping[] = [
|
||||
'id' => (int)$parent['id'],
|
||||
'name' => $parent['name'],
|
||||
'db_field' => 'amount_p' . $num,
|
||||
'css' => 'p' . $num,
|
||||
'color' => $parent['color'] ?? (($num === 1) ? '#0891b2' : '#f59e0b')
|
||||
];
|
||||
}
|
||||
|
||||
// 2. Récupération Config Salaires
|
||||
$salaryConfig = [];
|
||||
$stmt = $pdo->prepare("SELECT * FROM pf_salary_config WHERE year = ?");
|
||||
$stmt->execute([$currentYear]);
|
||||
@@ -11,16 +30,17 @@ while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$salaryConfig[$row['person']] = $row;
|
||||
}
|
||||
|
||||
foreach (['Alex', 'Laia'] as $p) {
|
||||
if (!isset($salaryConfig[$p])) {
|
||||
$salaryConfig[$p] = ['salary'=>0, 'mensualite'=>0, 'frais_func'=>0, 'eco_perso'=>0, 'eco_family'=>0];
|
||||
// Sécurité profils de salaire vides
|
||||
foreach ($parentMapping as $map) {
|
||||
if (!isset($salaryConfig[$map['name']])) {
|
||||
$salaryConfig[$map['name']] = ['salary'=>0, 'mensualite'=>0, 'frais_func'=>0, 'eco_perso'=>0, 'eco_family'=>0];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Récupération Catégories
|
||||
// 3. Récupération Catégories
|
||||
$cats = $pdo->query("SELECT * FROM pf_alloc_categories ORDER BY sort_order ASC, id ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 3. Gestion du mois FOCUS et Navigation
|
||||
// 4. Gestion du mois FOCUS et Navigation
|
||||
$focusDate = isset($_GET['focus_date']) ? $_GET['focus_date'] : date('Y-m-01');
|
||||
$focusTs = strtotime($focusDate);
|
||||
|
||||
@@ -32,7 +52,7 @@ for ($i = 0; $i < 6; $i++) {
|
||||
$prevMonthLink = date('Y-m-01', strtotime("-1 month", $focusTs));
|
||||
$nextMonthLink = date('Y-m-01', strtotime("+1 month", $focusTs));
|
||||
|
||||
// NOUVEAU : Récupération des Cycles configurés dans pf_notes
|
||||
// Récupération des Cycles configurés dans pf_notes
|
||||
$cycleConfigs = [];
|
||||
$stmtNotes = $pdo->query("SELECT reference_id, content FROM pf_notes WHERE note_type = 'month_config'");
|
||||
while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
|
||||
@@ -43,7 +63,7 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Récupération Valeurs Répartition
|
||||
// 5. Récupération Valeurs Répartition
|
||||
$inQuery = implode(',', array_fill(0, count($months), '?'));
|
||||
$stmt = $pdo->prepare("SELECT * FROM pf_alloc_values WHERE month_date IN ($inQuery)");
|
||||
$stmt->execute($months);
|
||||
@@ -53,7 +73,7 @@ while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$allocs[$row['month_date']][$row['cat_id']] = $row;
|
||||
}
|
||||
|
||||
// 5. Récupération de l'ID de la catégorie système
|
||||
// 6. Récupération de l'ID de la catégorie système
|
||||
$sysCatId = null;
|
||||
foreach ($cats as $key => $c) {
|
||||
if ($c['name'] === 'SYSTEM_VALIDATION') {
|
||||
@@ -63,25 +83,26 @@ foreach ($cats as $key => $c) {
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Lecture des statuts de validation
|
||||
// Statuts de validation
|
||||
$focusDate = $months[0];
|
||||
$isValidatedAlex = false;
|
||||
$isValidatedLaia = false;
|
||||
|
||||
$isValidated = [];
|
||||
if ($sysCatId && isset($allocs[$focusDate][$sysCatId])) {
|
||||
$row = $allocs[$focusDate][$sysCatId];
|
||||
$isValidatedAlex = ($row['amount_alex'] == 1);
|
||||
$isValidatedLaia = ($row['amount_laia'] == 1);
|
||||
foreach ($parentMapping as $map) {
|
||||
$isValidated[$map['css']] = ($row[$map['db_field']] == 1);
|
||||
}
|
||||
} else {
|
||||
foreach ($parentMapping as $map) {
|
||||
$isValidated[$map['css']] = false;
|
||||
}
|
||||
}
|
||||
|
||||
$stmtNote = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'budget_prev' AND reference_id = ?");
|
||||
$stmtNote->execute([$focusDate]);
|
||||
$currentNote = $stmtNote->fetchColumn();
|
||||
|
||||
// 7. Récupération des vacances actives
|
||||
$activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN ('draft', 'planned', 'booked') ORDER BY start_date ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Helper function pour les mois
|
||||
function getTranslatedMonthName($dateString) {
|
||||
$m = date('m', strtotime($dateString));
|
||||
$y = date('Y', strtotime($dateString));
|
||||
@@ -89,7 +110,7 @@ function getTranslatedMonthName($dateString) {
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="prev-container">
|
||||
<div class="prev-container" style="--p1-main: <?= $parentMapping[0]['color'] ?>; --p2-main: <?= $parentMapping[1]['color'] ?>;">
|
||||
|
||||
<div>
|
||||
<div class="prev-section-header">
|
||||
@@ -101,35 +122,31 @@ function getTranslatedMonthName($dateString) {
|
||||
<tr>
|
||||
<th><?= tr('bud_prev_person') ?></th>
|
||||
<th><?= tr('bud_prev_salary') ?></th>
|
||||
<th><?= tr('bud_prev_monthly_payment') ?><br><small style="font-weight:normal; text-transform:none;">(<?= tr('bud_prev_joint_account') ?>)</small></th>
|
||||
<th><?= tr('bud_prev_monthly_payment') ?><br><small>(<?= tr('bud_prev_joint_account') ?>)</small></th>
|
||||
<th><?= tr('bud_prev_func_expenses') ?></th>
|
||||
<th><?= tr('bud_prev_perso_savings') ?></th>
|
||||
<th style="background:#f0f9ff; color:#0284c7;"><?= tr('bud_prev_family_savings') ?><br><small style="font-weight:normal; text-transform:none;">(<?= tr('bud_prev_available') ?>)</small></th>
|
||||
<th class="th-family-savings"><?= tr('bud_prev_family_savings') ?><br><small>(<?= tr('bud_prev_available') ?>)</small></th>
|
||||
<th><?= tr('bud_prev_perso_remaining') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
// Définition des noms à partir de la config
|
||||
$family_members = [ID_ALEX => 'Alex', ID_LAIA => 'Laia'];
|
||||
|
||||
foreach ($family_members as $id => $p):
|
||||
$d = $salaryConfig[$p];
|
||||
foreach ($parentMapping as $map):
|
||||
$d = $salaryConfig[$map['name']];
|
||||
$restant = $d['salary'] - ($d['mensualite'] + $d['frais_func'] + $d['eco_perso'] + $d['eco_family']);
|
||||
$borderColor = ($id === ID_ALEX) ? '#0891b2' : '#f59e0b';
|
||||
?>
|
||||
<tr data-person="<?= $p ?>">
|
||||
<td style="text-align:left; font-weight:bold; color:var(--text-main); border-left:4px solid <?= $borderColor ?>;">
|
||||
<?= $p ?>
|
||||
<tr data-person="<?= htmlspecialchars($map['name']) ?>">
|
||||
<td class="col-p<?= $map['css'] ?>">
|
||||
<?= htmlspecialchars($map['name']) ?>
|
||||
</td>
|
||||
<td><input type="number" class="prev-input" data-field="salary" value="<?= round($d['salary']) ?>" onchange="updateSalary('<?= $p ?>', this)"></td>
|
||||
<td><input type="number" class="prev-input" data-field="mensualite" value="<?= round($d['mensualite']) ?>" onchange="updateSalary('<?= $p ?>', this)"></td>
|
||||
<td><input type="number" class="prev-input" data-field="frais_func" value="<?= round($d['frais_func']) ?>" onchange="updateSalary('<?= $p ?>', this)"></td>
|
||||
<td><input type="number" class="prev-input" data-field="eco_perso" value="<?= round($d['eco_perso']) ?>" onchange="updateSalary('<?= $p ?>', this)"></td>
|
||||
<td style="background:#f0f9ff;">
|
||||
<input type="number" class="prev-input bold-blue" id="eco_family_<?= $p ?>" data-field="eco_family" value="<?= round($d['eco_family']) ?>" onchange="updateSalary('<?= $p ?>', this)">
|
||||
<td><input type="number" class="prev-input" data-field="salary" value="<?= round($d['salary']) ?>" onchange="updateSalary('<?= htmlspecialchars($map['name']) ?>', this)"></td>
|
||||
<td><input type="number" class="prev-input" data-field="mensualite" value="<?= round($d['mensualite']) ?>" onchange="updateSalary('<?= htmlspecialchars($map['name']) ?>', this)"></td>
|
||||
<td><input type="number" class="prev-input" data-field="frais_func" value="<?= round($d['frais_func']) ?>" onchange="updateSalary('<?= htmlspecialchars($map['name']) ?>', this)"></td>
|
||||
<td><input type="number" class="prev-input" data-field="eco_perso" value="<?= round($d['eco_perso']) ?>" onchange="updateSalary('<?= htmlspecialchars($map['name']) ?>', this)"></td>
|
||||
<td class="td-family-savings">
|
||||
<input type="number" class="prev-input bold-blue" id="eco_family_<?= $map['css'] ?>" data-field="eco_family" value="<?= round($d['eco_family']) ?>" onchange="updateSalary('<?= htmlspecialchars($map['name']) ?>', this)">
|
||||
</td>
|
||||
<td id="restant_<?= $p ?>" style="font-weight:bold; color:var(--text-muted); text-align:center;" class="sum-target">
|
||||
<td id="restant_<?= $map['css'] ?>" class="sum-target font-bold">
|
||||
<?= number_format($restant, 0, ',', ' ') ?> €
|
||||
</td>
|
||||
</tr>
|
||||
@@ -139,16 +156,16 @@ function getTranslatedMonthName($dateString) {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="prev-section-header" style="background: #f8fafc; padding: 15px; border-radius: 12px; border: 1px solid #e2e8f0; margin-bottom: 20px;">
|
||||
<div style="display:flex; gap:10px; align-items:center;">
|
||||
<h2 style="margin:0; font-size:1.3rem;"><?= tr('bud_prev_budget_alloc') ?></h2>
|
||||
<div class="nav-group" style="display:flex; gap:5px;">
|
||||
<div class="prev-section-header">
|
||||
<div class="prev-header-left">
|
||||
<h2><?= tr('bud_prev_budget_alloc') ?></h2>
|
||||
<div class="nav-group">
|
||||
<a href="?tab=budget_prev&focus_date=<?= $prevMonthLink ?>" class="btn-nav">◀</a>
|
||||
<a href="?tab=budget_prev&focus_date=<?= date('Y-m-01') ?>" class="btn-nav"><?= tr('bud_prev_today') ?></a>
|
||||
<a href="?tab=budget_prev&focus_date=<?= $nextMonthLink ?>" class="btn-nav">▶</a>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:10px;">
|
||||
<div class="prev-header-actions">
|
||||
<button type="button" class="pf-btn btn-secondary" onclick="duplicateMonth()">
|
||||
🔁 <?= tr('bud_sav_add_one_month') ?>
|
||||
</button>
|
||||
@@ -166,13 +183,14 @@ function getTranslatedMonthName($dateString) {
|
||||
<?php foreach ($months as $month):
|
||||
$isCurrent = ($month == date('Y-m-01'));
|
||||
$cls = $isCurrent ? 'current' : '';
|
||||
$colspan = count($parentMapping) + 1;
|
||||
?>
|
||||
<th colspan="3" class="th-month <?= $cls ?>">
|
||||
<span style="text-transform:capitalize;"><?= getTranslatedMonthName($month) ?></span>
|
||||
<th colspan="<?= $colspan ?>" class="th-month <?= $cls ?>">
|
||||
<span><?= getTranslatedMonthName($month) ?></span>
|
||||
<?php
|
||||
if (isset($cycleConfigs[$month]) && !empty($cycleConfigs[$month]['start_date'])) {
|
||||
$cStart = date('d/m', strtotime($cycleConfigs[$month]['start_date']));
|
||||
echo "<div style='font-size:0.75rem; font-weight:normal; color:#64748b; margin-top:2px;'>" . sprintf(tr('bud_sav_from_date'), $cStart) . "</div>";
|
||||
echo "<div class='cycle-start-label'>" . sprintf(tr('bud_sav_from_date'), $cStart) . "</div>";
|
||||
}
|
||||
?>
|
||||
</th>
|
||||
@@ -182,88 +200,76 @@ function getTranslatedMonthName($dateString) {
|
||||
<th class="col-sticky header-cell"></th>
|
||||
<?php foreach ($months as $month): ?>
|
||||
<th class="th-sub">Global</th>
|
||||
<th class="th-sub txt-alex">Alex</th>
|
||||
<th class="th-sub txt-laia">Laia</th>
|
||||
<?php foreach ($parentMapping as $map): ?>
|
||||
<th class="th-sub txt-<?= $map['css'] ?>"><?= htmlspecialchars($map['name']) ?></th>
|
||||
<?php endforeach; ?>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="row-total">
|
||||
<td class="col-sticky" style="text-align:right !important;">Total</td>
|
||||
<td class="col-sticky">Total</td>
|
||||
<?php foreach ($months as $m): ?>
|
||||
<td class="txt-global sum-target" style="border-left:2px solid #e2e8f0;" id="total_global_<?= $m ?>">0</td>
|
||||
<td class="txt-alex sum-target" id="total_alex_<?= $m ?>">0</td>
|
||||
<td class="txt-laia sum-target" id="total_laia_<?= $m ?>">0</td>
|
||||
<td class="txt-global sum-target" id="total_global_<?= $m ?>">0</td>
|
||||
<?php foreach ($parentMapping as $map): ?>
|
||||
<td class="txt-<?= $map['css'] ?> sum-target" id="total_<?= $map['css'] ?>_<?= $m ?>">0</td>
|
||||
<?php endforeach; ?>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
|
||||
<tr class="row-restant">
|
||||
<td class="col-sticky" style="text-align:right !important;"><?= tr('bud_prev_remaining') ?> (Eco Family)</td>
|
||||
<td class="col-sticky"><?= tr('bud_prev_remaining') ?> (Eco Family)</td>
|
||||
<?php foreach ($months as $m): ?>
|
||||
<td style="border-left:2px solid #cbd5e1; background:#e2e8f0;">-</td>
|
||||
<td class="val-ok sum-target" id="restant_alloc_alex_<?= $m ?>">0</td>
|
||||
<td class="val-ok sum-target" id="restant_alloc_laia_<?= $m ?>">0</td>
|
||||
<td class="td-separator">-</td>
|
||||
<?php foreach ($parentMapping as $map): ?>
|
||||
<td class="val-ok sum-target" id="restant_alloc_<?= $map['css'] ?>_<?= $m ?>">0</td>
|
||||
<?php endforeach; ?>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
|
||||
<?php foreach ($cats as $cat):
|
||||
$isIndicative = ($cat['name'] === 'Eco Alex' || $cat['name'] === 'Eco Laia');
|
||||
$isIndicative = (strpos($cat['name'], 'Eco P') === 0);
|
||||
$rowClass = $isIndicative ? 'row-indicative' : '';
|
||||
$inputClass = $isIndicative ? 'ignore-calc' : '';
|
||||
$rowStyle = $isIndicative ? 'background:#f8fafc; color:#94a3b8;' : '';
|
||||
|
||||
$catDisplayName = $cat['name'];
|
||||
if ($catDisplayName === 'Eco P1') $catDisplayName = 'Eco ' . $p1_name;
|
||||
if ($catDisplayName === 'Eco P2') $catDisplayName = 'Eco ' . $p2_name;
|
||||
?>
|
||||
<tr class="<?= $rowClass ?>" style="<?= $rowStyle ?>">
|
||||
<td class="col-sticky" style="position:relative; <?= $isIndicative ? 'opacity:0.8;' : '' ?>">
|
||||
<div style="font-weight:600; color:<?= $isIndicative ? '#64748b' : 'var(--text-main)' ?>;">
|
||||
<?= htmlspecialchars($cat['name']) ?>
|
||||
<tr class="<?= $rowClass ?>">
|
||||
<td class="col-sticky">
|
||||
<div class="cat-name-label">
|
||||
<?= htmlspecialchars($catDisplayName) ?>
|
||||
<?php if(!empty($cat['holiday_id'])) echo " 🌴"; ?>
|
||||
<?php if($isIndicative): ?><span style="font-size:0.7rem; border:1px solid #cbd5e1; border-radius:4px; padding:0 4px; margin-left:5px;">Info</span><?php endif; ?>
|
||||
<?php if($isIndicative): ?><span>Info</span><?php endif; ?>
|
||||
</div>
|
||||
<div style="font-size:0.75rem; color:var(--text-muted); font-style:italic; text-align:right;">
|
||||
<div class="cat-target-label">
|
||||
<?= htmlspecialchars($cat['target']) ?>
|
||||
</div>
|
||||
|
||||
<div class="row-actions">
|
||||
<button type="button"
|
||||
class="btn-icon-action edit"
|
||||
title="<?= tr('edit') ?>"
|
||||
data-id="<?= $cat['id'] ?>"
|
||||
data-name="<?= htmlspecialchars($cat['name']) ?>"
|
||||
data-target="<?= htmlspecialchars($cat['target']) ?>"
|
||||
data-holiday="<?= $cat['holiday_id'] ?? '' ?>"
|
||||
onclick="openEditModal(this)">
|
||||
✎
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
onclick="deleteCategory(<?= $cat['id'] ?>)"
|
||||
class="btn-icon-action delete" title="<?= tr('delete') ?>">🗑️
|
||||
</button>
|
||||
<button type="button" class="btn-icon-action edit" title="<?= tr('edit') ?>" data-id="<?= $cat['id'] ?>" data-name="<?= htmlspecialchars($cat['name']) ?>" data-target="<?= htmlspecialchars($cat['target']) ?>" data-holiday="<?= $cat['holiday_id'] ?? '' ?>" onclick="openEditModal(this)">✎</button>
|
||||
<button type="button" onclick="deleteCategory(<?= $cat['id'] ?>)" class="btn-icon-action delete" title="<?= tr('delete') ?>">🗑️</button>
|
||||
</div>
|
||||
</td>
|
||||
<?php foreach ($months as $m):
|
||||
$val = $allocs[$m][$cat['id']] ?? ['amount_alex'=>0, 'amount_laia'=>0];
|
||||
$val = $allocs[$m][$cat['id']] ?? [];
|
||||
?>
|
||||
<td class="txt-global sum-target" style="border-left:2px solid #e2e8f0; <?= $isIndicative?'color:#94a3b8; font-weight:normal;':'' ?>" id="g_<?= $m ?>_<?= $cat['id'] ?>">0</td>
|
||||
<td class="txt-global sum-target" id="g_<?= $m ?>_<?= $cat['id'] ?>">0</td>
|
||||
|
||||
<?php foreach ($parentMapping as $map):
|
||||
$dbField = $map['db_field'];
|
||||
$cellValue = isset($val[$dbField]) ? $val[$dbField] : 0;
|
||||
?>
|
||||
<td>
|
||||
<input type="number" step="1" class="prev-input txt-alex inp-alex-<?= $m ?> <?= $inputClass ?>"
|
||||
style="<?= $isIndicative ? 'color:#64748b;' : '' ?>"
|
||||
value="<?= $val['amount_alex'] == 0 ? '' : round($val['amount_alex']) ?>"
|
||||
<input type="number" step="1" class="prev-input txt-<?= $map['css'] ?> inp-<?= $map['css'] ?>-<?= $m ?> <?= $inputClass ?>"
|
||||
value="<?= $cellValue == 0 ? '' : round($cellValue) ?>"
|
||||
placeholder="-"
|
||||
data-target="<?= htmlspecialchars($cat['target']) ?>"
|
||||
onchange="updateAlloc('<?= $m ?>', <?= $cat['id'] ?>, 'amount_alex', this)">
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="number" step="1" class="prev-input txt-laia inp-laia-<?= $m ?> <?= $inputClass ?>"
|
||||
style="<?= $isIndicative ? 'color:#64748b;' : '' ?>"
|
||||
value="<?= $val['amount_laia'] == 0 ? '' : round($val['amount_laia']) ?>"
|
||||
placeholder="-"
|
||||
data-target="<?= htmlspecialchars($cat['target']) ?>"
|
||||
onchange="updateAlloc('<?= $m ?>', <?= $cat['id'] ?>, 'amount_laia', this)">
|
||||
onchange="updateAlloc('<?= $m ?>', <?= $cat['id'] ?>, '<?= $dbField ?>', this)">
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
@@ -271,120 +277,62 @@ function getTranslatedMonthName($dateString) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin: 20px 0; background: white; padding: 20px; border-radius: 12px; box-shadow: var(--shadow-sm); border: 1px solid #e2e8f0;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<h3 style="margin: 0; font-size: 1.1rem; color: #1e293b;">
|
||||
📝 <?= tr('bud_prev_notes_for') ?> <span style="text-transform:capitalize;"><?= getTranslatedMonthName($focusDate) ?></span>
|
||||
</h3>
|
||||
<span id="note-save-indicator" style="font-size:0.85rem; color:#10b981; font-weight:bold; opacity:0; transition:opacity 0.3s;">
|
||||
✓ <?= tr('bud_prev_saved') ?>
|
||||
</span>
|
||||
<div class="budget-notes-section">
|
||||
<div class="notes-header">
|
||||
<h3>📝 <?= tr('bud_prev_notes_for') ?> <span><?= getTranslatedMonthName($focusDate) ?></span></h3>
|
||||
<span id="note-save-indicator" class="note-save-indicator">✓ <?= tr('bud_prev_saved') ?></span>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
id="monthNoteArea"
|
||||
class="pf-input"
|
||||
rows="3"
|
||||
placeholder="<?= tr('bud_prev_notes_ph') ?>"
|
||||
style="width: 100%; resize: vertical; border-color: #cbd5e1; background: #f8fafc; margin-bottom: 10px;"
|
||||
><?= htmlspecialchars((string)$currentNote) ?></textarea>
|
||||
<textarea id="monthNoteArea" class="pf-input" rows="3" placeholder="<?= tr('bud_prev_notes_ph') ?>"><?= htmlspecialchars((string)$currentNote) ?></textarea>
|
||||
|
||||
<div style="text-align: right;">
|
||||
<button type="button" class="pf-btn" style="width: auto; display: inline-flex;"
|
||||
onclick="saveGenericNote('budget_prev', '<?= $focusDate ?>', document.getElementById('monthNoteArea').value)">
|
||||
<?= tr('bud_prev_save_note') ?>
|
||||
</button>
|
||||
<div class="notes-footer">
|
||||
<button type="button" class="pf-btn" onclick="saveGenericNote('budget_prev', '<?= $focusDate ?>', document.getElementById('monthNoteArea').value)"><?= tr('bud_prev_save_note') ?></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$focusMonth = $months[0];
|
||||
|
||||
$targetsOrder = ['vers commune', 'vers L.Pol', 'vers L.Pep', 'vers L.Perso'];
|
||||
|
||||
$allTargets = $targetsOrder;
|
||||
foreach($cats as $c) {
|
||||
$t = trim($c['target']);
|
||||
if(!empty($t) && !in_array($t, $allTargets)) {
|
||||
$allTargets[] = $t;
|
||||
}
|
||||
if(!empty($t) && !in_array($t, $allTargets)) { $allTargets[] = $t; }
|
||||
}
|
||||
$allTargets = array_unique($allTargets);
|
||||
|
||||
$summaryData = [];
|
||||
foreach($allTargets as $t) $summaryData[$t] = ['Alex' => 0, 'Laia' => 0];
|
||||
|
||||
foreach ($cats as $cat) {
|
||||
$t = trim($cat['target']);
|
||||
if (empty($t)) continue;
|
||||
|
||||
$val = $allocs[$focusMonth][$cat['id']] ?? ['amount_alex'=>0, 'amount_laia'=>0];
|
||||
|
||||
if (!isset($summaryData[$t])) {
|
||||
$summaryData[$t] = ['Alex' => 0, 'Laia' => 0];
|
||||
}
|
||||
|
||||
$summaryData[$t]['Alex'] += $val['amount_alex'];
|
||||
$summaryData[$t]['Laia'] += $val['amount_laia'];
|
||||
}
|
||||
|
||||
$grandTotalAlex = 0;
|
||||
$grandTotalLaia = 0;
|
||||
$grandTotalGlobal = 0;
|
||||
?>
|
||||
|
||||
<div class="recap-wrapper">
|
||||
<div class="recap-card">
|
||||
<div class="recap-header">
|
||||
<?= tr('bud_prev_transfers_to_make') ?> - <span style="text-transform:capitalize;"><?= getTranslatedMonthName($focusMonth) ?></span>
|
||||
<?= tr('bud_prev_transfers_to_make') ?> - <span><?= getTranslatedMonthName($focusMonth) ?></span>
|
||||
</div>
|
||||
|
||||
<table class="recap-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:left;"><?= tr('bud_prev_destination') ?></th>
|
||||
|
||||
<th class="col-alex">
|
||||
<div style="display:flex; flex-direction:column; align-items:center; gap:5px;">
|
||||
<span>ALEX</span>
|
||||
<?php if($isValidatedAlex): ?>
|
||||
<div style="background:#10b981; color:white; padding:4px 8px; border-radius:4px; font-size:0.7rem; display:flex; align-items:center; gap:4px;">
|
||||
✓ <?= tr('bud_prev_done') ?>
|
||||
</div>
|
||||
<th><?= tr('bud_prev_destination') ?></th>
|
||||
<?php foreach ($parentMapping as $map): ?>
|
||||
<th class="col-<?= $map['css'] ?>">
|
||||
<div class="recap-th-content">
|
||||
<span><?= htmlspecialchars($map['name']) ?></span>
|
||||
<?php if($isValidated[$map['css']]): ?>
|
||||
<div class="validation-badge">✓ <?= tr('bud_prev_done') ?></div>
|
||||
<?php else: ?>
|
||||
<button onclick="validateTransfers('Alex', '<?= $focusMonth ?>')" class="pf-btn btn-small" style="background:white; color:#0891b2; border:1px solid #0891b2; font-size:0.7rem; padding:2px 8px; height:auto;">
|
||||
<?= tr('bud_prev_validate') ?>
|
||||
</button>
|
||||
<button onclick="validateTransfers('<?= $map['css'] ?>', '<?= $focusMonth ?>')" class="pf-btn btn-small"><?= tr('bud_prev_validate') ?></button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th class="col-laia">
|
||||
<div style="display:flex; flex-direction:column; align-items:center; gap:5px;">
|
||||
<span>LAIA</span>
|
||||
<?php if($isValidatedLaia): ?>
|
||||
<div style="background:#10b981; color:white; padding:4px 8px; border-radius:4px; font-size:0.7rem; display:flex; align-items:center; gap:4px;">
|
||||
✓ <?= tr('bud_prev_done') ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<button onclick="validateTransfers('Laia', '<?= $focusMonth ?>')" class="pf-btn btn-small" style="background:white; color:#d97706; border:1px solid #d97706; font-size:0.7rem; padding:2px 8px; height:auto;">
|
||||
<?= tr('bud_prev_validate') ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<?php endforeach; ?>
|
||||
<th class="col-global">Global</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach($allTargets as $target):
|
||||
$tId = md5($target);
|
||||
?>
|
||||
<?php foreach($allTargets as $target): $tId = md5($target); ?>
|
||||
<tr id="row_summary_<?= $tId ?>">
|
||||
<td><?= htmlspecialchars($target) ?></td>
|
||||
<td class="col-alex sum-target" id="sum_alex_<?= $tId ?>">0 €</td>
|
||||
<td class="col-laia sum-target" id="sum_laia_<?= $tId ?>">0 €</td>
|
||||
<?php foreach ($parentMapping as $map): ?>
|
||||
<td class="col-<?= $map['css'] ?> sum-target" id="sum_<?= $map['css'] ?>_<?= $tId ?>">0 €</td>
|
||||
<?php endforeach; ?>
|
||||
<td class="col-global sum-target" id="sum_global_<?= $tId ?>">0 €</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
@@ -392,8 +340,9 @@ function getTranslatedMonthName($dateString) {
|
||||
<tfoot>
|
||||
<tr class="row-grand-total">
|
||||
<td><?= tr('bud_prev_grand_total') ?></td>
|
||||
<td class="col-alex sum-target" id="grand_total_alex">0 €</td>
|
||||
<td class="col-laia sum-target" id="grand_total_laia">0 €</td>
|
||||
<?php foreach ($parentMapping as $map): ?>
|
||||
<td class="col-<?= $map['css'] ?> sum-target" id="grand_total_<?= $map['css'] ?>">0 €</td>
|
||||
<?php endforeach; ?>
|
||||
<td class="col-global sum-target" id="grand_total_global">0 €</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
@@ -403,18 +352,18 @@ function getTranslatedMonthName($dateString) {
|
||||
</div>
|
||||
|
||||
<div id="addCatModal" class="pf-modal">
|
||||
<div class="pf-modal-content" style="background:white; width:95%; max-width:400px; border-radius:20px; box-shadow:0 20px 25px -5px rgba(0,0,0,0.1); padding:30px;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||||
<h3 class="pf-modal-title" style="margin:0; border:none; font-size:1.2rem;"><?= tr('bud_prev_new_line_title') ?></h3>
|
||||
<button onclick="document.getElementById('addCatModal').style.display='none'; document.body.classList.remove('no-scroll');" style="border:none; background:none; font-size:1.5rem; cursor:pointer; color:var(--text-muted);">×</button>
|
||||
<div class="pf-modal-content">
|
||||
<div class="prev-header-left">
|
||||
<h3 class="pf-modal-title"><?= tr('bud_prev_new_line_title') ?></h3>
|
||||
<button onclick="document.getElementById('addCatModal').style.display='none'; document.body.classList.remove('no-scroll');">×</button>
|
||||
</div>
|
||||
<form action="/modules/budget/includes/api/save-budget.php" method="POST">
|
||||
<input type="hidden" name="action" value="add_category">
|
||||
<div class="form-group" style="margin-bottom:15px;">
|
||||
<div class="form-group">
|
||||
<label class="pf-label"><?= tr('bud_prev_label_name') ?></label>
|
||||
<input type="text" name="name" class="pf-input" required>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:15px;">
|
||||
<div class="form-group">
|
||||
<label class="pf-label"><?= tr('bud_prev_label_target') ?></label>
|
||||
<select name="target" class="pf-input" required>
|
||||
<option value="" disabled selected>-- <?= tr('bud_prev_choose') ?> --</option>
|
||||
@@ -424,9 +373,9 @@ function getTranslatedMonthName($dateString) {
|
||||
<option value="vers commune">vers commune</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:20px;">
|
||||
<label class="pf-label" style="color:#8b5cf6;">🌴 <?= tr('bud_prev_link_holiday') ?></label>
|
||||
<select name="holiday_id" class="pf-input" style="border-color:#8b5cf6; background:#f5f3ff;">
|
||||
<div class="form-group">
|
||||
<label class="pf-label">🌴 <?= tr('bud_prev_link_holiday') ?></label>
|
||||
<select name="holiday_id" class="pf-input">
|
||||
<option value="">-- <?= tr('bud_prev_no_link') ?> --</option>
|
||||
<?php foreach ($activeHolidays as $hol): ?>
|
||||
<option value="<?= $hol['id'] ?>"><?= htmlspecialchars($hol['title']) ?></option>
|
||||
@@ -442,20 +391,19 @@ function getTranslatedMonthName($dateString) {
|
||||
</div>
|
||||
|
||||
<div id="editCatModal" class="pf-modal">
|
||||
<div class="pf-modal-content" style="max-width:400px;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||||
<h3 class="pf-modal-title" style="margin:0; border:none; font-size:1.2rem;"><?= tr('bud_prev_edit_line_title') ?></h3>
|
||||
<button onclick="document.getElementById('editCatModal').style.display='none'; document.body.classList.remove('no-scroll');" style="border:none; background:none; font-size:1.5rem; cursor:pointer; color:var(--text-muted);">×</button>
|
||||
<div class="pf-modal-content">
|
||||
<div class="prev-header-left">
|
||||
<h3 class="pf-modal-title"><?= tr('bud_prev_edit_line_title') ?></h3>
|
||||
<button onclick="document.getElementById('editCatModal').style.display='none'; document.body.classList.remove('no-scroll');">×</button>
|
||||
</div>
|
||||
<form action="/modules/budget/includes/api/save-budget.php" method="POST">
|
||||
<input type="hidden" name="action" value="update_category">
|
||||
<input type="hidden" name="cat_id" id="edit_cat_id">
|
||||
|
||||
<div class="form-group" style="margin-bottom:15px;">
|
||||
<div class="form-group">
|
||||
<label class="pf-label"><?= tr('bud_label_name') ?></label>
|
||||
<input type="text" name="name" id="edit_cat_name" class="pf-input" required>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:15px;">
|
||||
<div class="form-group">
|
||||
<label class="pf-label"><?= tr('bud_prev_label_target') ?></label>
|
||||
<select name="target" id="edit_cat_target" class="pf-input" required>
|
||||
<option value="vers L.Pol">vers L.Pol</option>
|
||||
@@ -464,9 +412,9 @@ function getTranslatedMonthName($dateString) {
|
||||
<option value="vers commune">vers commune</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:20px;">
|
||||
<label class="pf-label" style="color:#8b5cf6;">🌴 <?= tr('bud_prev_link_holiday') ?></label>
|
||||
<select name="holiday_id" id="edit_cat_holiday" class="pf-input" style="border-color:#8b5cf6; background:#f5f3ff;">
|
||||
<div class="form-group">
|
||||
<label class="pf-label">🌴 <?= tr('bud_prev_link_holiday') ?></label>
|
||||
<select name="holiday_id" id="edit_cat_holiday" class="pf-input">
|
||||
<option value="">-- <?= tr('bud_prev_no_link') ?> --</option>
|
||||
<?php foreach ($activeHolidays as $hol): ?>
|
||||
<option value="<?= $hol['id'] ?>"><?= htmlspecialchars($hol['title']) ?></option>
|
||||
@@ -481,20 +429,17 @@ function getTranslatedMonthName($dateString) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<button id="fabSumMode" class="pf-fab-sum" onclick="toggleSumMode()" title="<?= tr('bud_sav_sum_mode_title') ?>">
|
||||
<button id="fabSumMode" class="pf-fab-sum" onclick="toggleSumMode()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"></path><line x1="8" y1="12" x2="16" y2="12"></line><line x1="12" y1="8" x2="12" y2="16"></line></svg>
|
||||
</button>
|
||||
|
||||
<div id="sumResultBar" class="pf-sum-bar">
|
||||
<span class="pf-sum-label"><?= tr('bud_sav_selection') ?></span>
|
||||
<span id="sumResultValue" class="pf-sum-value">0 €</span>
|
||||
<button onclick="toggleSumMode()" class="pf-sum-close" title="<?= tr('btn_close') ?>">×</button>
|
||||
<button onclick="toggleSumMode()" class="pf-sum-close">×</button>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
// --- 1. SÉCURISATION TRADUCTIONS ET LANGUE ---
|
||||
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
||||
window.I18N = {
|
||||
...(window.I18N || {}),
|
||||
@@ -505,21 +450,17 @@ window.I18N = {
|
||||
'bud_err_tech': <?= json_encode(tr('bud_err_tech')) ?>
|
||||
};
|
||||
|
||||
window.CONFIG = window.CONFIG || {};
|
||||
window.CONFIG.parentMapping = <?= json_encode($parentMapping) ?>;
|
||||
|
||||
const currentYear = <?= $currentYear ?? 'new Date().getFullYear()' ?>;
|
||||
const months = <?= json_encode($months ?? []) ?>;
|
||||
const currentYear = <?= $currentYear ?>;
|
||||
const months = <?= json_encode($months) ?>;
|
||||
|
||||
function openEditModal(btn) {
|
||||
const id = btn.getAttribute('data-id');
|
||||
const name = btn.getAttribute('data-name');
|
||||
const target = btn.getAttribute('data-target');
|
||||
const holiday = btn.getAttribute('data-holiday');
|
||||
|
||||
document.getElementById('edit_cat_id').value = id;
|
||||
document.getElementById('edit_cat_name').value = name;
|
||||
document.getElementById('edit_cat_target').value = target;
|
||||
document.getElementById('edit_cat_holiday').value = holiday;
|
||||
|
||||
document.getElementById('edit_cat_id').value = btn.getAttribute('data-id');
|
||||
document.getElementById('edit_cat_name').value = btn.getAttribute('data-name');
|
||||
document.getElementById('edit_cat_target').value = btn.getAttribute('data-target');
|
||||
document.getElementById('edit_cat_holiday').value = btn.getAttribute('data-holiday');
|
||||
document.getElementById('editCatModal').style.display = 'flex';
|
||||
document.body.classList.add('no-scroll');
|
||||
}
|
||||
@@ -533,7 +474,11 @@ function updateSalary(person, input) {
|
||||
const ecoF = parseFloat(row.querySelector('[data-field="eco_family"]').value) || 0;
|
||||
|
||||
const restant = salary - (mens + frais + ecoP + ecoF);
|
||||
document.getElementById('restant_' + person).innerText = Math.round(restant).toLocaleString(window.appLang) + ' €';
|
||||
|
||||
const parentMap = window.CONFIG.parentMapping.find(m => m.name === person);
|
||||
if(parentMap) {
|
||||
document.getElementById('restant_' + parentMap.css).innerText = Math.round(restant).toLocaleString(window.appLang) + ' €';
|
||||
}
|
||||
|
||||
saveData('update_salary_config', { year: currentYear, person: person, field: input.dataset.field, value: input.value });
|
||||
recalcAllAllocations();
|
||||
@@ -547,6 +492,7 @@ function updateAlloc(month, catId, personField, input) {
|
||||
function duplicateMonth() {
|
||||
const targetDateStr = months[0];
|
||||
const sourceDateStr = months[1];
|
||||
const parentMap = window.CONFIG.parentMapping;
|
||||
|
||||
if (!sourceDateStr) {
|
||||
alert(window.I18N['bud_prev_err_no_history']);
|
||||
@@ -560,48 +506,48 @@ function duplicateMonth() {
|
||||
|
||||
const sourceName = formatMonth(sourceDateStr);
|
||||
const targetName = formatMonth(targetDateStr);
|
||||
|
||||
const message = window.I18N['bud_prev_confirm_copy'].replace('%s', sourceName).replace('%t', targetName);
|
||||
|
||||
if(!confirm(message)) return;
|
||||
|
||||
document.querySelectorAll('.inp-alex-' + sourceDateStr).forEach(sourceInput => {
|
||||
const firstCss = parentMap[0].css;
|
||||
document.querySelectorAll('.inp-' + firstCss + '-' + sourceDateStr).forEach(sourceInput => {
|
||||
const catIdMatch = sourceInput.getAttribute('onchange').match(/, (\d+),/);
|
||||
if(!catIdMatch) return;
|
||||
const catId = catIdMatch[1];
|
||||
const row = sourceInput.closest('tr');
|
||||
|
||||
const valAlex = sourceInput.value;
|
||||
const targetAlex = row.querySelector('.inp-alex-' + targetDateStr);
|
||||
if(targetAlex) {
|
||||
targetAlex.value = valAlex;
|
||||
updateAlloc(targetDateStr, catId, 'amount_alex', targetAlex);
|
||||
}
|
||||
|
||||
const valLaia = row.querySelector('.inp-laia-' + sourceDateStr).value;
|
||||
const targetLaia = row.querySelector('.inp-laia-' + targetDateStr);
|
||||
if(targetLaia) {
|
||||
targetLaia.value = valLaia;
|
||||
updateAlloc(targetDateStr, catId, 'amount_laia', targetLaia);
|
||||
parentMap.forEach(map => {
|
||||
const sInp = row.querySelector('.inp-' + map.css + '-' + sourceDateStr);
|
||||
const tInp = row.querySelector('.inp-' + map.css + '-' + targetDateStr);
|
||||
if(sInp && tInp) {
|
||||
tInp.value = sInp.value;
|
||||
updateAlloc(targetDateStr, catId, map.db_field, tInp);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function recalcAllAllocations() {
|
||||
const budgetAlex = parseFloat(document.getElementById('eco_family_Alex').value) || 0;
|
||||
const budgetLaia = parseFloat(document.getElementById('eco_family_Laia').value) || 0;
|
||||
const parentMap = window.CONFIG.parentMapping;
|
||||
|
||||
months.forEach(m => {
|
||||
let sumAlex = 0;
|
||||
let sumLaia = 0;
|
||||
|
||||
document.querySelectorAll('.inp-alex-' + m).forEach(inp => {
|
||||
const val = parseFloat(inp.value) || 0;
|
||||
if (!inp.classList.contains('ignore-calc')) sumAlex += val;
|
||||
let sums = {};
|
||||
parentMap.forEach(map => sums[map.css] = 0);
|
||||
|
||||
const firstCss = parentMap[0].css;
|
||||
document.querySelectorAll('.inp-' + firstCss + '-' + m).forEach(inp => {
|
||||
const row = inp.closest('tr');
|
||||
const laiaVal = parseFloat(row.querySelector('.inp-laia-' + m).value) || 0;
|
||||
const globalSum = val + laiaVal;
|
||||
let globalSum = 0;
|
||||
|
||||
parentMap.forEach(map => {
|
||||
const pInp = row.querySelector('.inp-' + map.css + '-' + m);
|
||||
const val = pInp ? (parseFloat(pInp.value) || 0) : 0;
|
||||
if (pInp && !pInp.classList.contains('ignore-calc')) {
|
||||
sums[map.css] += val;
|
||||
}
|
||||
globalSum += val;
|
||||
});
|
||||
|
||||
const onchangeStr = inp.getAttribute('onchange');
|
||||
const matches = onchangeStr.match(/, (\d+),/);
|
||||
@@ -611,73 +557,76 @@ function recalcAllAllocations() {
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.inp-laia-' + m).forEach(inp => {
|
||||
const val = parseFloat(inp.value) || 0;
|
||||
if (!inp.classList.contains('ignore-calc')) sumLaia += val;
|
||||
let totalGlobal = 0;
|
||||
parentMap.forEach(map => {
|
||||
const sumEl = document.getElementById('total_' + map.css + '_' + m);
|
||||
if(sumEl) sumEl.innerText = Math.round(sums[map.css]) + ' €';
|
||||
|
||||
totalGlobal += sums[map.css];
|
||||
|
||||
const budget = parseFloat(document.getElementById('eco_family_' + map.css).value) || 0;
|
||||
const rest = budget - sums[map.css];
|
||||
|
||||
const elRest = document.getElementById('restant_alloc_' + map.css + '_' + m);
|
||||
if (elRest) {
|
||||
elRest.innerText = Math.round(rest) + ' €';
|
||||
elRest.className = 'val-' + (rest >= 0 ? 'ok' : 'ko') + ' sum-target';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('total_alex_' + m).innerText = Math.round(sumAlex) + ' €';
|
||||
document.getElementById('total_laia_' + m).innerText = Math.round(sumLaia) + ' €';
|
||||
document.getElementById('total_global_' + m).innerText = Math.round(sumAlex + sumLaia) + ' €';
|
||||
|
||||
const restAlex = budgetAlex - sumAlex;
|
||||
const restLaia = budgetLaia - sumLaia;
|
||||
|
||||
const elRestAlex = document.getElementById('restant_alloc_alex_' + m);
|
||||
const elRestLaia = document.getElementById('restant_alloc_laia_' + m);
|
||||
|
||||
elRestAlex.innerText = Math.round(restAlex) + ' €';
|
||||
elRestAlex.className = 'val-' + (restAlex >= 0 ? 'ok' : 'ko') + ' sum-target';
|
||||
|
||||
elRestLaia.innerText = Math.round(restLaia) + ' €';
|
||||
elRestLaia.className = 'val-' + (restLaia >= 0 ? 'ok' : 'ko') + ' sum-target';
|
||||
const globEl = document.getElementById('total_global_' + m);
|
||||
if(globEl) globEl.innerText = Math.round(totalGlobal) + ' €';
|
||||
});
|
||||
|
||||
updateSummaryTable();
|
||||
if(isSumModeActive) updateSumResult();
|
||||
}
|
||||
|
||||
function updateSummaryTable() {
|
||||
const focusMonth = months[0];
|
||||
let grandTotalAlex = 0;
|
||||
let grandTotalLaia = 0;
|
||||
const dataByTarget = {};
|
||||
const parentMap = window.CONFIG.parentMapping;
|
||||
let grandTotals = {};
|
||||
let dataByTarget = {};
|
||||
|
||||
document.querySelectorAll('.inp-alex-' + focusMonth).forEach(inp => {
|
||||
parentMap.forEach(map => {
|
||||
grandTotals[map.css] = 0;
|
||||
dataByTarget[map.css] = {};
|
||||
|
||||
document.querySelectorAll('.inp-' + map.css + '-' + focusMonth).forEach(inp => {
|
||||
const target = inp.getAttribute('data-target');
|
||||
if(target) {
|
||||
if(!dataByTarget[target]) dataByTarget[target] = { alex: 0, laia: 0 };
|
||||
dataByTarget[target].alex += (parseFloat(inp.value) || 0);
|
||||
if(!dataByTarget[map.css][target]) dataByTarget[map.css][target] = 0;
|
||||
dataByTarget[map.css][target] += (parseFloat(inp.value) || 0);
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.inp-laia-' + focusMonth).forEach(inp => {
|
||||
const target = inp.getAttribute('data-target');
|
||||
if(target) {
|
||||
if(!dataByTarget[target]) dataByTarget[target] = { alex: 0, laia: 0 };
|
||||
dataByTarget[target].laia += (parseFloat(inp.value) || 0);
|
||||
}
|
||||
});
|
||||
|
||||
const tbody = document.querySelector('.recap-table tbody');
|
||||
if(tbody) {
|
||||
Array.from(tbody.rows).forEach(row => {
|
||||
const targetName = row.cells[0].innerText.trim();
|
||||
const alexSum = dataByTarget[targetName] ? dataByTarget[targetName].alex : 0;
|
||||
const laiaSum = dataByTarget[targetName] ? dataByTarget[targetName].laia : 0;
|
||||
const globalSum = alexSum + laiaSum;
|
||||
let globalSum = 0;
|
||||
|
||||
row.cells[1].innerText = Math.round(alexSum).toLocaleString(window.appLang) + ' ' + window.CONFIG.CURRENCY;
|
||||
row.cells[2].innerText = Math.round(laiaSum).toLocaleString(window.appLang) + ' ' + window.CONFIG.CURRENCY;
|
||||
row.cells[3].innerText = Math.round(globalSum).toLocaleString(window.appLang) + ' ' + window.CONFIG.CURRENCY;
|
||||
parentMap.forEach((map, idx) => {
|
||||
const pSum = dataByTarget[map.css][targetName] || 0;
|
||||
row.cells[idx + 1].innerText = Math.round(pSum).toLocaleString(window.appLang) + ' ' + window.CONFIG.CURRENCY;
|
||||
grandTotals[map.css] += pSum;
|
||||
globalSum += pSum;
|
||||
});
|
||||
|
||||
grandTotalAlex += alexSum;
|
||||
grandTotalLaia += laiaSum;
|
||||
row.cells[parentMap.length + 1].innerText = Math.round(globalSum).toLocaleString(window.appLang) + ' ' + window.CONFIG.CURRENCY;
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('grand_total_alex').innerText = Math.round(grandTotalAlex).toLocaleString(window.appLang) + ' ' + window.CONFIG.CURRENCY;
|
||||
document.getElementById('grand_total_laia').innerText = Math.round(grandTotalLaia).toLocaleString(window.appLang) + ' ' + window.CONFIG.CURRENCY;
|
||||
document.getElementById('grand_total_global').innerText = Math.round(grandTotalAlex + grandTotalLaia).toLocaleString(window.appLang) + ' ' + window.CONFIG.CURRENCY;
|
||||
let totalGrandGlobal = 0;
|
||||
parentMap.forEach(map => {
|
||||
const grandEl = document.getElementById('grand_total_' + map.css);
|
||||
if(grandEl) grandEl.innerText = Math.round(grandTotals[map.css]).toLocaleString(window.appLang) + ' ' + window.CONFIG.CURRENCY;
|
||||
totalGrandGlobal += grandTotals[map.css];
|
||||
});
|
||||
|
||||
const globGrandEl = document.getElementById('grand_total_global');
|
||||
if(globGrandEl) globGrandEl.innerText = Math.round(totalGrandGlobal).toLocaleString(window.appLang) + ' ' + window.CONFIG.CURRENCY;
|
||||
}
|
||||
|
||||
function saveData(action, data) {
|
||||
@@ -687,19 +636,19 @@ function saveData(action, data) {
|
||||
fetch('modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData });
|
||||
}
|
||||
|
||||
function validateTransfers(person, month) {
|
||||
const msg = window.I18N['bud_prev_confirm_transfers'].replace('%p', person).replace('%m', month);
|
||||
function validateTransfers(personCss, month) {
|
||||
const msg = window.I18N['bud_prev_confirm_transfers'].replace('%p', personCss).replace('%m', month);
|
||||
if (!confirm(msg)) return;
|
||||
|
||||
const parentMap = window.CONFIG.parentMapping.find(m => m.css === personCss);
|
||||
const dbPersonName = parentMap ? parentMap.name : personCss;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'validate_transfers');
|
||||
formData.append('person', person);
|
||||
formData.append('person', dbPersonName);
|
||||
formData.append('month_date', month);
|
||||
|
||||
fetch('modules/budget/includes/api/save-budget.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
fetch('modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if(data.success) window.location.reload();
|
||||
@@ -715,19 +664,11 @@ function saveGenericNote(noteType, refId, content) {
|
||||
formData.append('reference_id', refId);
|
||||
formData.append('content', content);
|
||||
|
||||
fetch('modules/budget/includes/api/save-budget.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
fetch('modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData })
|
||||
.then(async r => {
|
||||
const text = await r.text();
|
||||
if (!r.ok) throw new Error(`Erreur HTTP ${r.status} : ${text}`);
|
||||
if (!text) throw new Error("Le serveur a renvoyé une réponse vide.");
|
||||
try {
|
||||
if (!r.ok) throw new Error(`Erreur HTTP ${r.status}`);
|
||||
return JSON.parse(text);
|
||||
} catch(e) {
|
||||
throw new Error("Réponse inattendue (non JSON) : " + text);
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
if(data.success) {
|
||||
@@ -737,13 +678,10 @@ function saveGenericNote(noteType, refId, content) {
|
||||
setTimeout(() => indicator.style.opacity = '0', 2000);
|
||||
}
|
||||
} else {
|
||||
alert("Erreur lors de la sauvegarde : " + data.error);
|
||||
alert("Erreur: " + data.error);
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
console.error('Erreur technique:', e);
|
||||
alert(e.message);
|
||||
});
|
||||
.catch(e => alert(e.message));
|
||||
}
|
||||
|
||||
let isSumModeActive = false;
|
||||
@@ -751,10 +689,8 @@ let selectedElementsForSum = new Set();
|
||||
|
||||
function toggleSumMode() {
|
||||
isSumModeActive = !isSumModeActive;
|
||||
|
||||
const fab = document.getElementById('fabSumMode');
|
||||
const resultBar = document.getElementById('sumResultBar');
|
||||
|
||||
if (isSumModeActive) {
|
||||
fab.classList.add('active');
|
||||
document.body.classList.add('sum-mode-active');
|
||||
@@ -764,7 +700,6 @@ function toggleSumMode() {
|
||||
fab.classList.remove('active');
|
||||
document.body.classList.remove('sum-mode-active');
|
||||
resultBar.classList.remove('visible');
|
||||
|
||||
selectedElementsForSum.forEach(el => el.classList.remove('sum-selected'));
|
||||
selectedElementsForSum.clear();
|
||||
}
|
||||
@@ -772,53 +707,34 @@ function toggleSumMode() {
|
||||
|
||||
function extractNumberFromText(text) {
|
||||
if (!text) return 0;
|
||||
const cleanText = text.replace(',', '.').replace(/[^\d.-]/g, '');
|
||||
return parseFloat(cleanText) || 0;
|
||||
return parseFloat(text.replace(',', '.').replace(/[^\d.-]/g, '')) || 0;
|
||||
}
|
||||
|
||||
function updateSumResult() {
|
||||
let total = 0;
|
||||
selectedElementsForSum.forEach(el => {
|
||||
let val = 0;
|
||||
if (el.tagName === 'INPUT') {
|
||||
val = parseFloat(el.value) || 0;
|
||||
} else {
|
||||
val = extractNumberFromText(el.innerText);
|
||||
}
|
||||
total += val;
|
||||
total += (el.tagName === 'INPUT') ? (parseFloat(el.value) || 0) : extractNumberFromText(el.innerText);
|
||||
});
|
||||
|
||||
document.getElementById('sumResultValue').innerText = Math.round(total).toLocaleString(window.appLang) + ' ' + window.CONFIG.CURRENCY;
|
||||
}
|
||||
|
||||
async function deleteCategory(id) {
|
||||
if (!confirm(tr('bud_prev_confirm_del_line'))) return;
|
||||
|
||||
if (!confirm(window.I18N['bud_prev_confirm_del_line'])) return;
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'delete_category');
|
||||
formData.append('id', id);
|
||||
formData.append('ajax', '1'); //
|
||||
|
||||
formData.append('ajax', '1');
|
||||
try {
|
||||
const result = await pachaFetch('modules/budget/includes/api/save-budget.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
window.location.reload();
|
||||
}
|
||||
const result = await pachaFetch('modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData });
|
||||
if (result.success) window.location.reload();
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!isSumModeActive) return;
|
||||
|
||||
const targetElement = e.target.closest('input[type="number"], .sum-target');
|
||||
|
||||
if (targetElement) {
|
||||
e.preventDefault();
|
||||
|
||||
if (selectedElementsForSum.has(targetElement)) {
|
||||
selectedElementsForSum.delete(targetElement);
|
||||
targetElement.classList.remove('sum-selected');
|
||||
@@ -826,36 +742,24 @@ document.addEventListener('click', function(e) {
|
||||
selectedElementsForSum.add(targetElement);
|
||||
targetElement.classList.add('sum-selected');
|
||||
}
|
||||
|
||||
updateSumResult();
|
||||
}
|
||||
}, true);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', recalcAllAllocations);
|
||||
|
||||
// --- INTERCEPTION ASYNCHRONE DES FORMULAIRES DE MODALES ---
|
||||
document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form => {
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const originalText = submitBtn.innerText;
|
||||
|
||||
try {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerText = '⏳ ...';
|
||||
|
||||
const formData = new FormData(form);
|
||||
formData.append('ajax', '1');
|
||||
|
||||
const actionUrl = form.getAttribute('action');
|
||||
|
||||
// On utilise pachaFetch au lieu de fetch
|
||||
const result = await pachaFetch(actionUrl, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await pachaFetch(actionUrl, { method: 'POST', body: formData });
|
||||
if (result.success) {
|
||||
form.closest('.pf-modal').style.display = 'none';
|
||||
document.body.classList.remove('no-scroll');
|
||||
@@ -864,7 +768,6 @@ document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form
|
||||
alert((window.I18N['bud_err_tech'] || 'Erreur') + " : " + (result.error || "Inconnue"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur Fetch Modale:", error);
|
||||
alert("Une erreur technique est survenue.");
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
|
||||
@@ -5,32 +5,42 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const CONGE_TYPES = ["OFF_CAROLE", "EXTRA_OFF_CAROLE"];
|
||||
const GUARDE_TYPES = ["CENTRE", "AVIS"];
|
||||
const PEP_TYPES = ["PEP_SICK"];
|
||||
const FAMILY = {
|
||||
ALEX: { id: 2, prefix: "alex" },
|
||||
LAIA: { id: 3, prefix: "laia" },
|
||||
};
|
||||
|
||||
// 🟢 Liste globale dynamique des personnes (fini les IDs en dur !)
|
||||
const peopleList = window.CONFIG.PEOPLE || [];
|
||||
|
||||
// On filtre pour ne garder que les parents pour la logique principale des congés
|
||||
const parents = peopleList.filter(
|
||||
(p) => p.role && p.role.toLowerCase() === "parent",
|
||||
);
|
||||
|
||||
const LEAVES_CONFIG = {
|
||||
CP: {
|
||||
startMonth: 8, // Le cycle commence en août (après la tolérance de juillet)
|
||||
startMonth: 8,
|
||||
defaultBalance: 25,
|
||||
},
|
||||
JRA: {
|
||||
// Tu pourras ajouter les années suivantes ici
|
||||
yearlyTotals: {
|
||||
2024: 10, // ex: 0.83 * 12 arrondi
|
||||
2024: 10,
|
||||
2025: 10,
|
||||
2026: 11, // ex: 0.9 * 12 arrondi
|
||||
2026: 11,
|
||||
},
|
||||
defaultBalance: 10,
|
||||
toleranceMonths: 2, // Janvier et Février
|
||||
toleranceMonths: 2,
|
||||
maxReport: 2,
|
||||
},
|
||||
JA: {
|
||||
[FAMILY.ALEX.id]: { startMonth: 4, startDay: 29, defaultBalance: 4 },
|
||||
[FAMILY.LAIA.id]: { startMonth: 10, startDay: 1, defaultBalance: 4 }, // Date de Laia à adapter
|
||||
},
|
||||
JA: {}, // Ce tableau est maintenant vide par défaut, on le remplit dynamiquement juste en dessous
|
||||
};
|
||||
|
||||
// 🟢 INITIALISATION DYNAMIQUE DES JOURS D'ANNIVERSAIRE (JA)
|
||||
parents.forEach((parent) => {
|
||||
LEAVES_CONFIG.JA[parent.id] = {
|
||||
startMonth: 4, // Mois par défaut (à faire évoluer en BDD au Niveau 3)
|
||||
startDay: 29, // Jour par défaut
|
||||
defaultBalance: 4,
|
||||
};
|
||||
});
|
||||
|
||||
class FamilyCalendar {
|
||||
constructor() {
|
||||
this.planningBody = document.getElementById("planningBody");
|
||||
@@ -455,12 +465,17 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const d = new Date(l.leave_date + "T00:00:00");
|
||||
if (d >= w.dayDates.mon && d <= w.dayDates.fri) {
|
||||
const dur = parseFloat(l.duration) || 1;
|
||||
const prefix =
|
||||
l.person_id === FAMILY.ALEX.id
|
||||
? FAMILY.ALEX.prefix
|
||||
: l.person_id === FAMILY.LAIA.id
|
||||
? FAMILY.LAIA.prefix
|
||||
: null;
|
||||
|
||||
// 🟢 Correspondance dynamique pour le stockage des totaux de la semaine
|
||||
let prefix = null;
|
||||
if (parents[0] && parseInt(l.person_id) === parseInt(parents[0].id))
|
||||
prefix = "alex";
|
||||
else if (
|
||||
parents[1] &&
|
||||
parseInt(l.person_id) === parseInt(parents[1].id)
|
||||
)
|
||||
prefix = "laia";
|
||||
|
||||
if (prefix) w.totals[`${prefix}${l.leave_type}`] += dur;
|
||||
}
|
||||
});
|
||||
@@ -479,17 +494,16 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
}
|
||||
|
||||
calculateMonthlyBalances() {
|
||||
const balances = {
|
||||
[FAMILY.ALEX.id]: { CP: {}, JRA: {}, JA: {} },
|
||||
[FAMILY.LAIA.id]: { CP: {}, JRA: {}, JA: {} },
|
||||
};
|
||||
const balances = {};
|
||||
// 🟢 On initialise le tableau des soldes pour chaque parent trouvé
|
||||
parents.forEach((p) => {
|
||||
balances[p.id] = { CP: {}, JRA: {}, JA: {} };
|
||||
});
|
||||
|
||||
// Liste des mois actuellement affichés dans le planning
|
||||
const ymSet = new Set();
|
||||
this.weeks.forEach((w) => ymSet.add(w.monthKey));
|
||||
const ymList = Array.from(ymSet).sort();
|
||||
|
||||
// Pré-calcul de l'utilisation par mois
|
||||
const usageByMonth = {};
|
||||
this.leaves.forEach((l) => {
|
||||
const pid = l.person_id;
|
||||
@@ -502,7 +516,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
(usageByMonth[pid][type][ym] || 0) + parseFloat(l.duration);
|
||||
});
|
||||
|
||||
[FAMILY.ALEX.id, FAMILY.LAIA.id].forEach((pid) => {
|
||||
parents.forEach((parent) => {
|
||||
const pid = parent.id;
|
||||
["CP", "JRA", "JA"].forEach((type) => {
|
||||
ymList.forEach((ym) => {
|
||||
const [currYear, currMonth] = ym.split("-").map(Number);
|
||||
@@ -510,8 +525,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
let cycleStartStr = "";
|
||||
let initialBalance = 0;
|
||||
|
||||
// --- 1. RECHERCHE D'UN CORRECTIF MANUEL (SNAPSHOT) ---
|
||||
// On cherche le snapshot le plus récent qui est inférieur ou égal au mois en cours de calcul
|
||||
const latestSnapshot = (this.leaveSnapshots || [])
|
||||
.filter(
|
||||
(s) =>
|
||||
@@ -519,18 +532,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
s.leave_type == type &&
|
||||
s.snapshot_date.substring(0, 7) <= ym,
|
||||
)
|
||||
// On trie du plus récent au plus ancien pour prendre le premier
|
||||
.sort((a, b) =>
|
||||
b.snapshot_date.localeCompare(a.snapshot_date),
|
||||
)[0];
|
||||
|
||||
if (latestSnapshot) {
|
||||
// Si on trouve un correctif, il devient notre nouveau "point zéro"
|
||||
cycleStartStr = latestSnapshot.snapshot_date.substring(0, 7);
|
||||
initialBalance = parseFloat(latestSnapshot.remaining_balance); // Utilisation de VOTRE nom de colonne
|
||||
}
|
||||
// --- 2. SINON, CALCUL CLASSIQUE PAR DÉFAUT ---
|
||||
else {
|
||||
initialBalance = parseFloat(latestSnapshot.remaining_balance);
|
||||
} else {
|
||||
if (type === "CP") {
|
||||
const refYear =
|
||||
currMonth >= LEAVES_CONFIG.CP.startMonth
|
||||
@@ -552,7 +561,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
LEAVES_CONFIG.JRA.yearlyTotals[currYear] ||
|
||||
LEAVES_CONFIG.JRA.defaultBalance;
|
||||
|
||||
// Logique de report du reliquat de l'année N-1
|
||||
if (currMonth <= LEAVES_CONFIG.JRA.toleranceMonths) {
|
||||
const prevYear = currYear - 1;
|
||||
const prevInitial =
|
||||
@@ -569,14 +577,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
0,
|
||||
prevInitial - usedPrevYear,
|
||||
);
|
||||
// Ajout du report plafonné à 2 jours
|
||||
initialBalance += Math.min(
|
||||
remainingPrevYear,
|
||||
LEAVES_CONFIG.JRA.maxReport,
|
||||
);
|
||||
console.log(
|
||||
`[JRA] Personne ${pid}, Année ${currYear}: Report de ${Math.min(remainingPrevYear, LEAVES_CONFIG.JRA.maxReport)}j inclus.`,
|
||||
);
|
||||
}
|
||||
} else if (type === "JA") {
|
||||
const configJA = LEAVES_CONFIG.JA[pid];
|
||||
@@ -598,11 +602,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3. DÉDUCTION DES CONGÉS PRIS DEPUIS LE POINT ZÉRO ---
|
||||
let usedBeforeCurrentMonth = 0;
|
||||
|
||||
Object.keys(usageByMonth[pid]?.[type] || {}).forEach((usedYm) => {
|
||||
// On ne déduit que ce qui a été posé entre le début du cycle (ou la date du snapshot) et le mois en cours
|
||||
if (usedYm >= cycleStartStr && usedYm < ym) {
|
||||
usedBeforeCurrentMonth += usageByMonth[pid][type][usedYm];
|
||||
}
|
||||
@@ -688,10 +689,28 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const dayLeaves = this.leaves.filter((l) => l.leave_date === iso);
|
||||
if (dayLeaves.length) {
|
||||
let html = `<div style="position:absolute; bottom:0; left:0; width:100%; font-size:9px; line-height:1; display:flex; justify-content:center; gap:2px; pointer-events:none;">`;
|
||||
if (dayLeaves.some((l) => l.person_id === window.CONFIG.ID_ALEX))
|
||||
html += `<span style="color:#0f766e; font-weight:800;">A</span>`;
|
||||
if (dayLeaves.some((l) => l.person_id === window.CONFIG.ID_LAIA))
|
||||
html += `<span style="color:#b45309; font-weight:800;">L</span>`;
|
||||
|
||||
const colors = [
|
||||
"#0f766e",
|
||||
"#b45309",
|
||||
"#047857",
|
||||
"#4338ca",
|
||||
"#b91c1c",
|
||||
];
|
||||
if (window.CONFIG.PEOPLE && window.CONFIG.PEOPLE.length > 0) {
|
||||
window.CONFIG.PEOPLE.forEach((person, index) => {
|
||||
if (
|
||||
dayLeaves.some(
|
||||
(l) => parseInt(l.person_id) === parseInt(person.id),
|
||||
)
|
||||
) {
|
||||
const color = colors[index % colors.length];
|
||||
const initial = person.name.charAt(0).toUpperCase();
|
||||
html += `<span style="color:${color}; font-weight:800; margin: 0 1px;">${initial}</span>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
td.innerHTML += html;
|
||||
}
|
||||
@@ -731,8 +750,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
tr.appendChild(tdUse);
|
||||
});
|
||||
};
|
||||
renderPersonCols(FAMILY.ALEX.id, `col-${FAMILY.ALEX.prefix}`);
|
||||
renderPersonCols(FAMILY.LAIA.id, `col-${FAMILY.LAIA.prefix}`);
|
||||
parents.forEach((parent, index) => {
|
||||
const cssPrefix = index % 2 === 0 ? "col-alex" : "col-laia";
|
||||
renderPersonCols(parent.id, cssPrefix);
|
||||
});
|
||||
}
|
||||
this.planningBody.appendChild(tr);
|
||||
});
|
||||
@@ -845,9 +866,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
container.style.display = "flex";
|
||||
let html = "";
|
||||
|
||||
[FAMILY.ALEX, FAMILY.LAIA].forEach((person) => {
|
||||
parents.forEach((person) => {
|
||||
html += `<div class="fc-minimal-balance-card">
|
||||
<strong class="${person.prefix}">${person.prefix.toUpperCase()}</strong>
|
||||
<strong class="col-alex">${person.name.toUpperCase()}</strong>
|
||||
<div class="fc-minimal-chips">`;
|
||||
|
||||
["CP", "JRA", "JA"].forEach((type) => {
|
||||
@@ -989,10 +1010,19 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const dayLeaves = this.leaves.filter((l) => l.leave_date === iso);
|
||||
if (dayLeaves.length) {
|
||||
content += `<div style="position:absolute; bottom:2px; left:2px; font-size:10px; font-weight:bold;">`;
|
||||
if (dayLeaves.some((l) => l.person_id === 2))
|
||||
content += `<span style="color:#0f766e">A</span> `;
|
||||
if (dayLeaves.some((l) => l.person_id === 3))
|
||||
content += `<span style="color:#b45309">L</span>`;
|
||||
|
||||
parents.forEach((parent, index) => {
|
||||
if (
|
||||
dayLeaves.some(
|
||||
(l) => parseInt(l.person_id) === parseInt(parent.id),
|
||||
)
|
||||
) {
|
||||
const color = index % 2 === 0 ? "#0f766e" : "#b45309";
|
||||
const initial = parent.name.charAt(0).toUpperCase();
|
||||
content += `<span style="color:${color}">${initial}</span> `;
|
||||
}
|
||||
});
|
||||
|
||||
content += `</div>`;
|
||||
}
|
||||
content += `</div>`;
|
||||
@@ -1557,7 +1587,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
if (dates.includes(e.date)) activeEvents.add(e.type);
|
||||
});
|
||||
|
||||
const activeLeaves = { 2: new Set(), 3: new Set() };
|
||||
const activeLeaves = {};
|
||||
parents.forEach((p) => (activeLeaves[p.id] = new Set()));
|
||||
|
||||
this.leaves.forEach((l) => {
|
||||
if (dates.includes(l.leave_date)) {
|
||||
if (activeLeaves[l.person_id])
|
||||
@@ -1623,32 +1655,33 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
<button class="fc-menu-btn ${getBtnClass("PEP_SICK")}" data-action="add" data-type="PEP_SICK" style="width:100%">${getBtnIcon("PEP_SICK")}${tr("leg_pep_sick")} 🤒</button>
|
||||
</div>`;
|
||||
|
||||
// 5. Section Enfants
|
||||
// 5. Section Parents (Congés)
|
||||
html += `<div class="fc-menu-section">
|
||||
${buildHeader(tr("fc_menu_kids_leaves"), null, null)}
|
||||
<div class="fc-menu-leaves-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<tr>`;
|
||||
|
||||
// En-têtes dynamiques (Noms + Bouton Poubelle)
|
||||
parents.forEach((parent) => {
|
||||
html += `<th>
|
||||
<div class="fc-th-inline">
|
||||
Alex <button class="fc-menu-clear-icon fc-menu-btn-th" data-action="clear-leaves-person" data-pid="2" title="${tr("fc_clear")} Alex">${trashSvg}</button>
|
||||
${parent.name} <button class="fc-menu-clear-icon fc-menu-btn-th" data-action="clear-leaves-person" data-pid="${parent.id}" title="${tr("fc_clear")} ${parent.name}">${trashSvg}</button>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="fc-th-inline">
|
||||
Laia <button class="fc-menu-clear-icon fc-menu-btn-th" data-action="clear-leaves-person" data-pid="3" title="${tr("fc_clear")} Laia">${trashSvg}</button>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</th>`;
|
||||
});
|
||||
html += ` </tr>
|
||||
</thead>
|
||||
<tbody>`;
|
||||
|
||||
// Lignes de congés (CP, JRA, JA)
|
||||
["CP", "JRA", "JA"].forEach((t) => {
|
||||
html += `<tr>
|
||||
<td><button class="fc-menu-btn ${getBtnClass(t, 2)}" data-action="add-leave" data-pid="2" data-type="${t}">${getBtnIcon(t, 2)}${t}</button></td>
|
||||
<td><button class="fc-menu-btn ${getBtnClass(t, 3)}" data-action="add-leave" data-pid="3" data-type="${t}">${getBtnIcon(t, 3)}${t}</button></td>
|
||||
</tr>`;
|
||||
html += `<tr>`;
|
||||
parents.forEach((parent) => {
|
||||
html += `<td><button class="fc-menu-btn ${getBtnClass(t, parent.id)}" data-action="add-leave" data-pid="${parent.id}" data-type="${t}">${getBtnIcon(t, parent.id)}${t}</button></td>`;
|
||||
});
|
||||
html += `</tr>`;
|
||||
});
|
||||
|
||||
html += ` </tbody>
|
||||
@@ -1743,23 +1776,19 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
},
|
||||
);
|
||||
} else if (action === "clear-leaves") {
|
||||
if (window.CONFIG.PEOPLE && window.CONFIG.PEOPLE.length > 0) {
|
||||
for (const person of window.CONFIG.PEOPLE) {
|
||||
await this.postApi(
|
||||
"/modules/family-calendar/includes/api/manage-leaf.php",
|
||||
{
|
||||
action: "bulk_delete_day_person",
|
||||
dates,
|
||||
person_id: window.CONFIG.ID_ALEX,
|
||||
},
|
||||
);
|
||||
await this.postApi(
|
||||
"/modules/family-calendar/includes/api/manage-leaf.php",
|
||||
{
|
||||
action: "bulk_delete_day_person",
|
||||
dates,
|
||||
person_id: window.CONFIG.ID_LAIA,
|
||||
person_id: person.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.refreshAllData();
|
||||
} catch (e) {
|
||||
|
||||
@@ -51,3 +51,4 @@
|
||||
[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"}]
|
||||
|
||||
+7
-7
@@ -13,10 +13,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
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
CREATE TABLE `pf_people` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(50) NOT NULL,
|
||||
`user_id` int(11) DEFAULT NULL,
|
||||
`role` varchar(50) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
||||
|
||||
-- ─── Calendrier familial ──────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_events (
|
||||
@@ -232,9 +235,6 @@ SET FOREIGN_KEY_CHECKS = 1;
|
||||
INSERT IGNORE INTO pf_users (id, username, password_hash, display_name)
|
||||
VALUES (1, 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin');
|
||||
|
||||
-- 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');
|
||||
|
||||
-- ─── Calendar iOS ─────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_calendar_events (
|
||||
|
||||
+42
-15
@@ -69,13 +69,25 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
}
|
||||
|
||||
// 🟢 ENRICHISSEMENT : Prise en compte de la couleur du profil
|
||||
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 {
|
||||
// MÀJ dans la base Meta (users)
|
||||
$meta_pdo->prepare("UPDATE users SET display_name = ? WHERE id = ?")
|
||||
->execute([$display_name, $user_id]);
|
||||
|
||||
// MÀJ dans la base locale Foyer (pf_people) via le user_id mappé
|
||||
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.";
|
||||
}
|
||||
@@ -129,7 +141,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.";
|
||||
@@ -225,9 +236,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
|
||||
// ─── Chargement données ───────────────────────────────────────────────────────
|
||||
$user = $meta_pdo->prepare("SELECT * FROM users WHERE id = ?");
|
||||
$user->execute([$user_id]);
|
||||
$user = $user->fetch();
|
||||
|
||||
// 1. On charge la base locale AVANT pour que son $user n'écrase pas le nôtre
|
||||
if ($family_id) {
|
||||
require_once __DIR__ . '/includes/db.php';
|
||||
}
|
||||
|
||||
// 2. Maintenant on peut charger ton profil en toute sécurité
|
||||
$stmtUser = $meta_pdo->prepare("SELECT * FROM users WHERE id = ?");
|
||||
$stmtUser->execute([$user_id]);
|
||||
$user = $stmtUser->fetch();
|
||||
|
||||
// 3. Récupération de la couleur actuelle depuis pf_people
|
||||
$user_color = '#0891b2'; // Fallback par défaut
|
||||
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 = [];
|
||||
@@ -276,7 +305,6 @@ require __DIR__ . '/header.php';
|
||||
<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 +331,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>
|
||||
@@ -413,7 +440,6 @@ require __DIR__ . '/header.php';
|
||||
<?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; }
|
||||
@@ -452,15 +478,12 @@ require __DIR__ . '/header.php';
|
||||
<?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';
|
||||
@@ -502,7 +525,6 @@ require __DIR__ . '/header.php';
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<!-- ── Profil ──────────────────────────────────────────────────────────── -->
|
||||
<section class="pf-panel-card">
|
||||
<h2 class="pf-card-h2">👤 Mon profil</h2>
|
||||
<form method="post">
|
||||
@@ -516,11 +538,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>
|
||||
|
||||
<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">Enregistrer</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 +573,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 +588,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 +610,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): ?>
|
||||
|
||||
Reference in New Issue
Block a user