Compare commits
91
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3eae9e1d4 | ||
|
|
4cfef07dc6 | ||
|
|
3c615e3021 | ||
|
|
094a5b2b91 | ||
|
|
dc8a2e5244 | ||
|
|
8a7b5b6644 | ||
|
|
a2e0904132 | ||
|
|
cfe8962bd4 | ||
|
|
42ccb89b66 | ||
|
|
21c9b37ae0 | ||
|
|
c05802fc79 | ||
|
|
348a8f1811 | ||
|
|
28a98f5790 | ||
|
|
32c291ff2c | ||
|
|
b3139b577a | ||
|
|
f19a7442de | ||
|
|
536f479423 | ||
|
|
685c649e7c | ||
|
|
074d8e12d2 | ||
|
|
f748330a1b | ||
|
|
ebdbee71db | ||
|
|
659b286967 | ||
|
|
ac8ebf7328 | ||
|
|
49d4ec7c35 | ||
|
|
c48a6ba116 | ||
|
|
20d6c9b7b0 | ||
|
|
d856d0a409 | ||
|
|
71ec8bd68a | ||
|
|
86ef3ccbf2 | ||
|
|
e7aa3b595b | ||
|
|
3e98de9c18 | ||
|
|
6b2f83b68c | ||
|
|
7e24ca7612 | ||
|
|
506cd64a44 | ||
|
|
041411b070 | ||
|
|
fa6521f8f5 | ||
|
|
5618448602 | ||
|
|
9d22dc29b4 | ||
|
|
b3a4614c59 | ||
|
|
18740c5e1b | ||
|
|
854c6388ba | ||
|
|
1e090cf9ca | ||
|
|
ff83320f9a | ||
|
|
557a1451de | ||
|
|
03a97df8f1 | ||
|
|
ad5a845f18 | ||
|
|
142d4612a9 | ||
|
|
2e30569675 | ||
|
|
f5c5320189 | ||
|
|
5b4f4c68b2 | ||
|
|
aad528d15b | ||
|
|
89d8ad114b | ||
|
|
f36bfa828e | ||
|
|
f9aa368768 | ||
|
|
e73524b2ae | ||
|
|
96df90d372 | ||
|
|
d6e95c1b2a | ||
|
|
16b08dd03b | ||
|
|
6459c225ac | ||
|
|
429a4ccb66 | ||
|
|
cd79bbb71d | ||
|
|
ecac9479d5 | ||
|
|
429a964727 | ||
|
|
70462aa78b | ||
|
|
afa6587eb3 | ||
|
|
c26a369bb6 | ||
|
|
305b58653d | ||
|
|
8e3e7fe3d8 | ||
|
|
2c8bbbb3e1 | ||
|
|
0ff3c42d35 | ||
|
|
ea04831451 | ||
|
|
d9ae5308ea | ||
|
|
2c5fb9da6b | ||
|
|
751082308a | ||
|
|
b44688a654 | ||
|
|
f8e2ad67ee | ||
|
|
6aab112ecb | ||
|
|
6e409fb3a1 | ||
|
|
535f1b7dcf | ||
|
|
4130b03a68 | ||
|
|
315e1b5fb1 | ||
|
|
b1d3db4892 | ||
|
|
99ed0e31de | ||
|
|
a1e7bc62fd | ||
|
|
3675f0c01a | ||
|
|
836533fcf6 | ||
|
|
5fe9046a63 | ||
|
|
5b70cd20e6 | ||
|
|
ed2d5a7c9e | ||
|
|
6bbee271e3 | ||
|
|
7b294f300a |
No files matched your search
@@ -2,7 +2,7 @@ name: Deploy HouseHub
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main, staging-perco, staging-fefe]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
@@ -15,6 +15,21 @@ jobs:
|
|||||||
username: perco
|
username: perco
|
||||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
script: |
|
script: |
|
||||||
cd /opt/container/househub
|
BRANCH="${{ github.ref_name }}"
|
||||||
git pull
|
case "$BRANCH" in
|
||||||
echo "✓ HouseHub mis à jour"
|
main)
|
||||||
|
cd /opt/container/househub
|
||||||
|
git pull
|
||||||
|
echo "✓ Production mise à jour"
|
||||||
|
;;
|
||||||
|
staging-perco)
|
||||||
|
cd /opt/container/househub-perco
|
||||||
|
git pull origin staging-perco
|
||||||
|
echo "✓ Staging Perco mis à jour"
|
||||||
|
;;
|
||||||
|
staging-fefe)
|
||||||
|
cd /opt/container/househub-fefe
|
||||||
|
git pull origin staging-fefe
|
||||||
|
echo "✓ Staging Fefe mis à jour"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
househub_db_data
|
househub_db_data
|
||||||
.env
|
.env
|
||||||
data/.hh_app_secret
|
data/.hh_app_secret
|
||||||
|
extract_source.php
|
||||||
|
.htaccess
|
||||||
@@ -151,12 +151,15 @@ L'architecture de HouseHub isole chaque foyer dans sa propre base de données (`
|
|||||||
Afin de transformer complètement l'application héritée de Pacha Family en un produit générique et autonome pour chaque espace familial, les chantiers suivants doivent être menés.
|
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)
|
🟢 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] 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.
|
[ ] 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.
|
[ ] 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)
|
🟡 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.
|
[ ] 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).
|
[ ] 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.
|
[ ] 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.
|
[ ] Types de carburants (Garage) : Variabiliser la liste statique des carburants dans garage.php.
|
||||||
|
|
||||||
🔴 Niveau 3 : Complexe (Logique métier et Schéma SQL)
|
🔴 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.
|
[ ] 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.
|
[ ] 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.
|
[ ] 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.
|
||||||
@@ -174,14 +179,15 @@ Afin de transformer complètement l'application héritée de Pacha Family en un
|
|||||||
|
|
||||||
## 📌 Versioning
|
## 📌 Versioning
|
||||||
|
|
||||||
| Version | Changements |
|
| Version | Changements |
|
||||||
| ------- | ------------------------------------------------------------------------------------------ |
|
| ------- | ------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| v2.2.0 | Multi-tenant (p.1) : Paramètres dynamiques du foyer (€, Zone) via BDD + script migration |
|
| v2.3.0 | Multi-tenant (p.2) : Refonte BDD Budget (p1, p2), suppression IDs statiques et thèmes de profils dynamiques (color-mix()) |
|
||||||
| v2.1.0 | Module Planka : Kanban natif connecté à Planka via API proxy PHP |
|
| v2.2.0 | Multi-tenant (p.1) : Paramètres dynamiques du foyer (€, Zone) via BDD + script migration |
|
||||||
| v2.0.0 | Module PrintVault : viewer 3D STL/3MF/GCode, stockage local, trajectoires GCode illimitées |
|
| v2.1.0 | Module Planka : Kanban natif connecté à Planka via API proxy PHP |
|
||||||
| v1.5.0 | Modules Todo (rappels Discord), Mémo, Courses (liste partagée) |
|
| v2.0.0 | Module PrintVault : viewer 3D STL/3MF/GCode, stockage local, trajectoires GCode illimitées |
|
||||||
| v1.4.0 | Synchronisation CalDAV iOS |
|
| v1.5.0 | Modules Todo (rappels Discord), Mémo, Courses (liste partagée) |
|
||||||
| v1.3.0 | Optimisation SQL — suppression des requêtes N+1, temps de réponse ÷3 |
|
| v1.4.0 | Synchronisation CalDAV iOS |
|
||||||
| v1.2.0 | Modernisation UI — Toasts, confirmations sur-mesure |
|
| v1.3.0 | Optimisation SQL — suppression des requêtes N+1, temps de réponse ÷3 |
|
||||||
| v1.1.0 | Refonte expérience mobile (Bottom Sheets) |
|
| v1.2.0 | Modernisation UI — Toasts, confirmations sur-mesure |
|
||||||
| v1.0.0 | Version stable initiale — Budget, Calendrier, Voyages, Cadeaux, Garage |
|
| v1.1.0 | Refonte expérience mobile (Bottom Sheets) |
|
||||||
|
| v1.0.0 | Version stable initiale — Budget, Calendrier, Voyages, Cadeaux, Garage |
|
||||||
+35
-27
@@ -4,6 +4,18 @@ require __DIR__ . '/includes/db.php';
|
|||||||
require_once __DIR__ . '/includes/i18n.php';
|
require_once __DIR__ . '/includes/i18n.php';
|
||||||
require_login('/login.php');
|
require_login('/login.php');
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// DIAGNOSTIC ONBOARDING (Vérifie si le budget est prêt à être utilisé)
|
||||||
|
// ============================================================================
|
||||||
|
$currentYear = date('Y');
|
||||||
|
$hasCategories = ((int)$pdo->query("SELECT COUNT(*) FROM pf_budget_categories")->fetchColumn() > 0);
|
||||||
|
$hasSalaries = ((int)$pdo->query("SELECT COUNT(*) FROM pf_salary_config WHERE year = $currentYear")->fetchColumn() > 0);
|
||||||
|
$hasAccounts = ((int)$pdo->query("SELECT COUNT(*) FROM pf_bank_accounts")->fetchColumn() > 0);
|
||||||
|
$hasBudgetItems = ((int)$pdo->query("SELECT COUNT(*) FROM pf_budget_items")->fetchColumn() > 0);
|
||||||
|
|
||||||
|
// Le minimum vital pour que ça tourne
|
||||||
|
$isBudgetSetupOk = ($hasCategories && $hasSalaries && $hasAccounts);
|
||||||
|
|
||||||
// Gestion de l'onglet actif (par défaut 'suivi')
|
// Gestion de l'onglet actif (par défaut 'suivi')
|
||||||
$tab = $_GET['tab'] ?? 'suivi';
|
$tab = $_GET['tab'] ?? 'suivi';
|
||||||
|
|
||||||
@@ -15,8 +27,17 @@ require __DIR__ . '/header.php';
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="pf-container">
|
<div class="pf-container">
|
||||||
<div class="pf-hero" style="text-align: center; margin-bottom: 30px;">
|
<div class="pf-hero budget-hero">
|
||||||
<h1 style="margin-bottom: 20px;"><?= tr('budget_main_header') ?></h1>
|
<div class="budget-title-group">
|
||||||
|
<h1><?= tr('budget_main_header') ?></h1>
|
||||||
|
<!-- Bouton avec séparation claire entre l'engrenage animable et l'alerte fixe -->
|
||||||
|
<button class="btn-header-settings" onclick="openBudgetSettings()" title="<?= tr('settings') ?>">
|
||||||
|
<span class="gear-icon-anim">⚙️</span>
|
||||||
|
<?php if (!$isBudgetSetupOk || !$hasBudgetItems): ?>
|
||||||
|
<span class="alert-dot">!</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<nav class="budget-tabs-container">
|
<nav class="budget-tabs-container">
|
||||||
<a href="?tab=suivi" class="tab-item <?= $tab == 'suivi' ? 'active' : '' ?>">
|
<a href="?tab=suivi" class="tab-item <?= $tab == 'suivi' ? 'active' : '' ?>">
|
||||||
@@ -38,13 +59,18 @@ require __DIR__ . '/header.php';
|
|||||||
<span class="tab-icon">📊</span>
|
<span class="tab-icon">📊</span>
|
||||||
<span><?= tr('budget_tab_recap') ?></span>
|
<span><?= tr('budget_tab_recap') ?></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a href="?tab=provisions" class="tab-item <?= $tab == 'provisions' ? 'active' : '' ?>">
|
||||||
|
<span class="tab-icon">🧮</span>
|
||||||
|
<span><?= tr('budget_tab_provisions') ?></span>
|
||||||
|
</a>
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="pf-section">
|
<section class="pf-section">
|
||||||
<?php
|
<?php
|
||||||
// ROUTAGE DYNAMIQUE
|
$allowedTabs = ['recap', 'suivi', 'epargne', 'budget_prev', 'provisions'];
|
||||||
$allowedTabs = ['recap', 'suivi', 'epargne', 'budget_prev'];
|
|
||||||
|
|
||||||
if (in_array($tab, $allowedTabs)) {
|
if (in_array($tab, $allowedTabs)) {
|
||||||
$viewPath = __DIR__ . "/modules/budget/views/" . $tab . ".php";
|
$viewPath = __DIR__ . "/modules/budget/views/" . $tab . ".php";
|
||||||
@@ -52,7 +78,7 @@ require __DIR__ . '/header.php';
|
|||||||
if (file_exists($viewPath)) {
|
if (file_exists($viewPath)) {
|
||||||
include $viewPath;
|
include $viewPath;
|
||||||
} else {
|
} else {
|
||||||
echo "<div style='text-align:center; padding:50px; color:#ef4444;'>";
|
echo "<div class='budget-error-msg'>";
|
||||||
echo "<h3>" . tr('budget_err_file_not_found') . "</h3>";
|
echo "<h3>" . tr('budget_err_file_not_found') . "</h3>";
|
||||||
echo "<p>" . sprintf(tr('budget_err_file_detail'), htmlspecialchars($tab)) . "</p>";
|
echo "<p>" . sprintf(tr('budget_err_file_detail'), htmlspecialchars($tab)) . "</p>";
|
||||||
echo "</div>";
|
echo "</div>";
|
||||||
@@ -63,26 +89,8 @@ require __DIR__ . '/header.php';
|
|||||||
?>
|
?>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
|
||||||
// --- AUTO-SCROLL DES ONGLETS MOBILES ---
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const tabsContainer = document.querySelector('.budget-tabs-container');
|
|
||||||
const activeTab = document.querySelector('.tab-item.active');
|
|
||||||
|
|
||||||
// Si on a bien un conteneur d'onglets ET un onglet actif
|
<?php
|
||||||
if (tabsContainer && activeTab) {
|
require __DIR__ . '/modules/budget/views/settings_modal.php';
|
||||||
// On vérifie si le conteneur a un scroll horizontal possible
|
require __DIR__ . '/footer.php';
|
||||||
if (tabsContainer.scrollWidth > tabsContainer.clientWidth) {
|
?>
|
||||||
// On calcule la position pour centrer l'onglet à l'écran
|
|
||||||
const scrollPos = activeTab.offsetLeft - (tabsContainer.offsetWidth / 2) + (activeTab.offsetWidth / 2);
|
|
||||||
|
|
||||||
// On fait glisser le menu doucement
|
|
||||||
tabsContainer.scrollTo({
|
|
||||||
left: scrollPos,
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<?php require __DIR__ . '/footer.php'; ?>
|
|
||||||
+196
-113
@@ -1,127 +1,210 @@
|
|||||||
/* ════════════════════════════════════════════════════════════════════
|
/* ════════════════════════════════════════════════════════════════════
|
||||||
dark-mode.css — Dark mode overrides (all modules)
|
dark-mode.css — Dark mode overrides (Cleaned & Optimized)
|
||||||
Chargé en dernier dans le head pour garantir la cascade.
|
Chargé en dernier dans le head pour garantir la cascade.
|
||||||
════════════════════════════════════════════════════════════════════ */
|
════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
/* ─── HOME PAGE ─────────────────────────────────────────────────────────────── */
|
/* ─── FONDS & CONTENEURS PRINCIPAUX ────────────────────────────────── */
|
||||||
|
[data-theme="dark"] .pf-card,
|
||||||
|
[data-theme="dark"] .pf-modal-content,
|
||||||
|
[data-theme="dark"] .budget-tabs-container,
|
||||||
|
[data-theme="dark"] .fc-header-row,
|
||||||
|
[data-theme="dark"] .fc-month-nav-row,
|
||||||
|
[data-theme="dark"] .fc-month-calendar-wrapper,
|
||||||
|
[data-theme="dark"] .fc-month-balances,
|
||||||
|
[data-theme="dark"] .hol-idea-card,
|
||||||
|
[data-theme="dark"] .pf-holidays__titlebar,
|
||||||
|
[data-theme="dark"] .hol-filters-row,
|
||||||
|
[data-theme="dark"] .cl-titlebar,
|
||||||
|
[data-theme="dark"] .pf-child-section,
|
||||||
|
[data-theme="dark"] .pf-child-totals-bar,
|
||||||
|
[data-theme="dark"] .pf-gift-card-compact,
|
||||||
|
[data-theme="dark"] .epargne-grid-table,
|
||||||
|
[data-theme="dark"] .sticky-col {
|
||||||
|
background: var(--bg-panel) !important;
|
||||||
|
border-color: var(--border-light) !important;
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .owner-tabs,
|
||||||
|
[data-theme="dark"] .fc-view-controls,
|
||||||
|
[data-theme="dark"] .fc-day--other-month,
|
||||||
|
[data-theme="dark"] .fc-minimal-balance-card,
|
||||||
|
[data-theme="dark"] .pf-summary-pill,
|
||||||
|
[data-theme="dark"] .pf-filter-bar {
|
||||||
|
background: var(--bg-page) !important;
|
||||||
|
border-color: var(--border-light) !important;
|
||||||
|
color: var(--text-muted) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── TYPOGRAPHIE GLOBALE ──────────────────────────────────────────── */
|
||||||
|
[data-theme="dark"] h1,
|
||||||
|
[data-theme="dark"] h2,
|
||||||
|
[data-theme="dark"] h3,
|
||||||
|
[data-theme="dark"] .pf-modal-title,
|
||||||
|
[data-theme="dark"] .pf-card-title,
|
||||||
|
[data-theme="dark"] .hol-main-title,
|
||||||
|
[data-theme="dark"] .pf-gift-title,
|
||||||
|
[data-theme="dark"] .budget-view h2,
|
||||||
|
[data-theme="dark"] .view-header h2,
|
||||||
|
[data-theme="dark"] .month-header-container {
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .pf-card-desc,
|
||||||
|
[data-theme="dark"] .tab-item {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── INPUTS & SÉLECTEURS ──────────────────────────────────────────── */
|
||||||
|
[data-theme="dark"] .pf-input,
|
||||||
|
[data-theme="dark"] .pf-filter-select,
|
||||||
|
[data-theme="dark"] .hol-year-select,
|
||||||
|
[data-theme="dark"] .budget-view input,
|
||||||
|
[data-theme="dark"] .budget-view select,
|
||||||
|
[data-theme="dark"] .budget-view textarea,
|
||||||
|
[data-theme="dark"] .hol-idea-card input,
|
||||||
|
[data-theme="dark"] .hol-idea-card textarea,
|
||||||
|
[data-theme="dark"] .hol-idea-card select {
|
||||||
|
background: #1c2128 !important;
|
||||||
|
border-color: var(--border-light) !important;
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .pf-input::placeholder {
|
||||||
|
color: #484f58 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── BOUTONS & ÉTATS ACTIFS ───────────────────────────────────────── */
|
||||||
|
[data-theme="dark"] .pf-btn.btn-secondary,
|
||||||
|
[data-theme="dark"] .fc-nav-button,
|
||||||
|
[data-theme="dark"] .fc-view-button,
|
||||||
|
[data-theme="dark"] .hol-map-toggle,
|
||||||
|
[data-theme="dark"] .cl-view-btn,
|
||||||
|
[data-theme="dark"] .pf-btn-icon-text,
|
||||||
|
[data-theme="dark"] .owner-tab {
|
||||||
|
background: var(--bg-panel) !important;
|
||||||
|
color: var(--text-muted) !important;
|
||||||
|
border-color: var(--border-light) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .pf-btn.btn-secondary:hover,
|
||||||
|
[data-theme="dark"] .fc-nav-button:hover,
|
||||||
|
[data-theme="dark"] .hol-map-toggle:hover,
|
||||||
|
[data-theme="dark"] .pf-btn-icon-text:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.06) !important;
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .tab-item.active,
|
||||||
|
[data-theme="dark"] .fc-view-button--active,
|
||||||
|
[data-theme="dark"] .cl-view-btn.is-active {
|
||||||
|
background: rgba(59, 130, 246, 0.15) !important;
|
||||||
|
color: var(--primary) !important;
|
||||||
|
border-color: rgba(59, 130, 246, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .owner-tab.active {
|
||||||
|
background: var(--primary) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── TABLES ───────────────────────────────────────────────────────── */
|
||||||
|
[data-theme="dark"] .pf-table,
|
||||||
|
[data-theme="dark"] .fc-month-table,
|
||||||
|
[data-theme="dark"] #planningTable-wrapper {
|
||||||
|
background: var(--bg-panel) !important;
|
||||||
|
color: var(--text-main);
|
||||||
|
border-color: var(--border-light) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .pf-table thead th,
|
||||||
|
[data-theme="dark"] .fc-month-table th,
|
||||||
|
[data-theme="dark"] #planningTable thead th,
|
||||||
|
[data-theme="dark"] th.sticky-col,
|
||||||
|
[data-theme="dark"] .pf-child-header {
|
||||||
|
background: #1c2128 !important;
|
||||||
|
color: var(--text-muted) !important;
|
||||||
|
border-color: var(--border-light) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .pf-table tbody td,
|
||||||
|
[data-theme="dark"] .fc-month-table td,
|
||||||
|
[data-theme="dark"] #planningTable tbody td {
|
||||||
|
border-color: rgba(48, 54, 61, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .pf-table tbody tr:hover td,
|
||||||
|
[data-theme="dark"] .fc-month-table td:hover:not(.fc-day--other-month) {
|
||||||
|
background: rgba(255, 255, 255, 0.04) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .pf-table tfoot td {
|
||||||
|
background: #1c2128 !important;
|
||||||
|
border-color: rgba(48, 54, 61, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── MODULES SPÉCIFIQUES ──────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* Accueil */
|
||||||
[data-theme="dark"] body[data-page="home"] .pf-module-card {
|
[data-theme="dark"] body[data-page="home"] .pf-module-card {
|
||||||
background: rgba(22,27,34,.92) !important;
|
background: rgba(22, 27, 34, 0.92) !important;
|
||||||
border: 1px solid var(--border-light) !important;
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4) !important;
|
||||||
box-shadow: 0 4px 15px rgba(0,0,0,.4) !important;
|
|
||||||
}
|
}
|
||||||
[data-theme="dark"] body[data-page="home"] .pf-module-card:hover {
|
[data-theme="dark"] body[data-page="home"] .pf-module-card:hover {
|
||||||
border-color: var(--primary) !important;
|
border-color: var(--primary) !important;
|
||||||
}
|
}
|
||||||
[data-theme="dark"] body[data-page="home"] .pf-card-title { color: var(--text-main); }
|
|
||||||
[data-theme="dark"] body[data-page="home"] .pf-card-desc { color: var(--text-muted); }
|
|
||||||
[data-theme="dark"] body[data-page="home"] .pf-card-cta { color: var(--primary); }
|
|
||||||
|
|
||||||
/* ─── SHARED / GLOBAL PATTERNS ──────────────────────────────────────────────── */
|
/* Épargne & Grilles */
|
||||||
[data-theme="dark"] .pf-card { background: var(--bg-panel) !important; border-color: var(--border-light) !important; color: var(--text-main) !important; }
|
[data-theme="dark"] .eg-header-row {
|
||||||
[data-theme="dark"] .pf-table { background: var(--bg-panel); color: var(--text-main); }
|
background: #111827 !important;
|
||||||
[data-theme="dark"] .pf-table thead th { background: #1c2128 !important; color: var(--text-muted) !important; border-color: var(--border-light) !important; }
|
border-color: #30363d !important;
|
||||||
[data-theme="dark"] .pf-table tbody td { background: var(--bg-panel); color: var(--text-main); border-color: rgba(48,54,61,.5); }
|
color: #e6edf3 !important;
|
||||||
[data-theme="dark"] .pf-table tbody tr:hover td { background: rgba(255,255,255,.03); }
|
}
|
||||||
[data-theme="dark"] .pf-table tfoot,
|
[data-theme="dark"] .eg-row:nth-child(even) {
|
||||||
[data-theme="dark"] .pf-table tfoot td { background: #1c2128; color: var(--text-main); border-color: rgba(48,54,61,.5); }
|
background: rgba(255, 255, 255, 0.02);
|
||||||
[data-theme="dark"] .pf-modal-content { background: var(--bg-panel) !important; border: 1px solid var(--border-light) !important; color: var(--text-main) !important; }
|
}
|
||||||
[data-theme="dark"] .pf-modal-title { color: var(--text-main) !important; border-color: var(--border-light) !important; }
|
[data-theme="dark"] .eg-row:hover {
|
||||||
[data-theme="dark"] .pf-input,
|
background: rgba(255, 255, 255, 0.05);
|
||||||
[data-theme="dark"] .pf-filter-select,
|
}
|
||||||
[data-theme="dark"] .hol-year-select { background: #1c2128 !important; border-color: var(--border-light) !important; color: var(--text-main) !important; }
|
|
||||||
[data-theme="dark"] .pf-input::placeholder { color: #484f58 !important; }
|
|
||||||
[data-theme="dark"] .pf-btn.btn-secondary { background: var(--bg-panel) !important; color: var(--text-muted) !important; border-color: var(--border-light) !important; }
|
|
||||||
[data-theme="dark"] .pf-btn.btn-secondary:hover { background: rgba(255,255,255,.06) !important; color: var(--text-main) !important; }
|
|
||||||
|
|
||||||
/* ─── BUDGET MODULE ─────────────────────────────────────────────────────────── */
|
/* Éléments colorés spécifiques */
|
||||||
[data-theme="dark"] .budget-view { color: var(--text-main); }
|
[data-theme="dark"] .savings-table .row-extres td,
|
||||||
[data-theme="dark"] .budget-view h2,
|
[data-theme="dark"] .eg-extres-row {
|
||||||
[data-theme="dark"] .view-header h2 { color: var(--text-main) !important; }
|
background: rgba(245, 158, 11, 0.08) !important;
|
||||||
[data-theme="dark"] .budget-tabs-container { background: var(--bg-panel) !important; border-color: var(--border-light) !important; }
|
}
|
||||||
[data-theme="dark"] .tab-item { background: var(--bg-panel); color: var(--text-muted); border-color: var(--border-light); }
|
|
||||||
[data-theme="dark"] .tab-item:hover { background: rgba(255,255,255,.06); color: var(--text-main); }
|
|
||||||
[data-theme="dark"] .tab-item.active { background: rgba(59,130,246,.15) !important; color: var(--primary) !important; border-color: rgba(59,130,246,.3) !important; }
|
|
||||||
[data-theme="dark"] .owner-tabs { background: var(--bg-page); border-color: var(--border-light); }
|
|
||||||
[data-theme="dark"] .owner-tab { background: var(--bg-panel) !important; color: var(--text-muted) !important; border-color: var(--border-light) !important; }
|
|
||||||
[data-theme="dark"] .owner-tab.active { background: var(--primary) !important; color: #fff !important; }
|
|
||||||
[data-theme="dark"] .month-header-container { color: var(--text-main); }
|
|
||||||
/* sticky col: uses var(--bg-panel) already but th.sticky-col uses hardcoded #f8fafc */
|
|
||||||
[data-theme="dark"] th.sticky-col { background: #1c2128 !important; color: var(--text-muted) !important; }
|
|
||||||
[data-theme="dark"] .sticky-col { background: var(--bg-panel) !important; color: var(--text-main) !important; }
|
|
||||||
[data-theme="dark"] .month-actions .btn-icon-small { background: #1c2128 !important; border-color: var(--border-light) !important; color: var(--text-main) !important; }
|
|
||||||
[data-theme="dark"] .month-actions .btn-icon-small:hover { background: rgba(255,255,255,.06) !important; }
|
|
||||||
[data-theme="dark"] .savings-table .row-extres td { background: rgba(245,158,11,.08) !important; }
|
|
||||||
[data-theme="dark"] .nens-title.theme-pol { background: rgba(30,58,138,.2) !important; color: #93c5fd !important; border-color: #3b82f6 !important; }
|
|
||||||
[data-theme="dark"] .nens-title.theme-pep { background: rgba(20,83,45,.2) !important; color: #86efac !important; border-color: #22c55e !important; }
|
|
||||||
[data-theme="dark"] .nens-table.theme-pol thead th { background: rgba(30,58,138,.15) !important; color: #93c5fd !important; }
|
|
||||||
[data-theme="dark"] .nens-table.theme-pep thead th { background: rgba(20,83,45,.15) !important; color: #86efac !important; }
|
|
||||||
/* Specific input/selects in budget */
|
|
||||||
[data-theme="dark"] .budget-view input,
|
|
||||||
[data-theme="dark"] .budget-view select,
|
|
||||||
[data-theme="dark"] .budget-view textarea { background: #1c2128 !important; border-color: var(--border-light) !important; color: var(--text-main) !important; }
|
|
||||||
/* sum selected highlight */
|
|
||||||
[data-theme="dark"] .sum-selected { background: rgba(59,130,246,.1) !important; }
|
|
||||||
|
|
||||||
/* ─── FAMILY CALENDAR MODULE ────────────────────────────────────────────────── */
|
[data-theme="dark"] .nens-title.theme-pol {
|
||||||
[data-theme="dark"] .pf-family-calendar { color: var(--text-main); }
|
background: rgba(30, 58, 138, 0.2) !important;
|
||||||
[data-theme="dark"] .fc-header-row { background: var(--bg-panel); border-color: var(--border-light); }
|
color: #93c5fd !important;
|
||||||
[data-theme="dark"] .fc-view-controls { background: var(--bg-page) !important; border-color: var(--border-light) !important; }
|
border-color: #3b82f6 !important;
|
||||||
[data-theme="dark"] .fc-view-button { background: var(--bg-panel) !important; color: var(--text-muted) !important; border-color: var(--border-light) !important; }
|
}
|
||||||
[data-theme="dark"] .fc-view-button--active { background: rgba(59,130,246,.15) !important; color: var(--primary) !important; border-color: rgba(59,130,246,.3) !important; }
|
[data-theme="dark"] .nens-table.theme-pol thead th {
|
||||||
[data-theme="dark"] .fc-month-nav-row { background: var(--bg-panel); border-color: var(--border-light); }
|
background: rgba(30, 58, 138, 0.15) !important;
|
||||||
[data-theme="dark"] .fc-month-nav-row h3 { color: var(--text-main) !important; }
|
color: #93c5fd !important;
|
||||||
[data-theme="dark"] .fc-nav-button { background: var(--bg-panel) !important; color: var(--text-muted) !important; border-color: var(--border-light) !important; }
|
}
|
||||||
[data-theme="dark"] .fc-nav-button:hover { background: rgba(255,255,255,.06) !important; }
|
|
||||||
/* Calendar table — lots of !important in source */
|
|
||||||
[data-theme="dark"] .fc-month-table { border-color: var(--border-light) !important; }
|
|
||||||
[data-theme="dark"] .fc-month-table th { background: #1c2128 !important; color: var(--text-muted) !important; border-color: var(--border-light) !important; }
|
|
||||||
[data-theme="dark"] .fc-month-table td { background: var(--bg-panel) !important; border-color: rgba(48,54,61,.5) !important; }
|
|
||||||
[data-theme="dark"] .fc-month-table td:hover:not(.fc-day--other-month) { background: rgba(255,255,255,.04) !important; }
|
|
||||||
[data-theme="dark"] .fc-day--other-month { background: var(--bg-page) !important; color: var(--text-muted) !important; }
|
|
||||||
[data-theme="dark"] .fc-month-calendar-wrapper,
|
|
||||||
[data-theme="dark"] .fc-month-balances { background: var(--bg-panel) !important; border-color: var(--border-light) !important; }
|
|
||||||
[data-theme="dark"] .fc-minimal-balance-card { background: var(--bg-page) !important; border-color: var(--border-light) !important; color: var(--text-main) !important; }
|
|
||||||
[data-theme="dark"] .fc-min-chip { background: var(--bg-panel) !important; border-color: var(--border-light) !important; }
|
|
||||||
[data-theme="dark"] .fc-burn-alert { background: rgba(239,68,68,.1) !important; color: #f87171 !important; }
|
|
||||||
/* Planning table (vue planning) */
|
|
||||||
[data-theme="dark"] #planningTable-wrapper { background: var(--bg-panel) !important; border-color: var(--border-light) !important; }
|
|
||||||
[data-theme="dark"] #planningTable thead th { background: #1c2128 !important; color: var(--text-muted) !important; }
|
|
||||||
[data-theme="dark"] #planningTable tbody td { border-color: rgba(48,54,61,.5) !important; }
|
|
||||||
[data-theme="dark"] #planningTable tbody td.col-sticky-mois,
|
|
||||||
[data-theme="dark"] #planningTable tbody td.col-sticky-sem { background: var(--bg-panel) !important; }
|
|
||||||
[data-theme="dark"] .col-month { background: var(--bg-panel) !important; color: var(--text-main) !important; }
|
|
||||||
[data-theme="dark"] .fc-week-nav-controls { background: var(--bg-panel) !important; border-color: var(--border-light) !important; }
|
|
||||||
[data-theme="dark"] .pf-btn-icon-text { background: var(--bg-panel) !important; color: var(--text-muted) !important; border-color: var(--border-light) !important; }
|
|
||||||
[data-theme="dark"] .pf-btn-icon-text:hover { background: rgba(255,255,255,.06) !important; }
|
|
||||||
|
|
||||||
/* ─── HOLIDAYS MODULE ───────────────────────────────────────────────────────── */
|
[data-theme="dark"] .nens-title.theme-pep {
|
||||||
[data-theme="dark"] .pf-holidays { color: var(--text-main); }
|
background: rgba(20, 83, 45, 0.2) !important;
|
||||||
[data-theme="dark"] .pf-holidays__titlebar { background: var(--bg-panel) !important; border-color: var(--border-light) !important; }
|
color: #86efac !important;
|
||||||
[data-theme="dark"] .hol-main-title { color: var(--text-main) !important; }
|
border-color: #22c55e !important;
|
||||||
[data-theme="dark"] .hol-idea-card { background: var(--bg-panel) !important; border-color: var(--border-light) !important; color: var(--text-main) !important; box-shadow: var(--shadow) !important; }
|
}
|
||||||
[data-theme="dark"] .hol-idea-card:hover { border-color: var(--primary) !important; }
|
[data-theme="dark"] .nens-table.theme-pep thead th {
|
||||||
[data-theme="dark"] .hol-idea-card input,
|
background: rgba(20, 83, 45, 0.15) !important;
|
||||||
[data-theme="dark"] .hol-idea-card textarea,
|
color: #86efac !important;
|
||||||
[data-theme="dark"] .hol-idea-card select { background: #1c2128 !important; border-color: var(--border-light) !important; color: var(--text-main) !important; }
|
}
|
||||||
[data-theme="dark"] .hol-filters-row { background: var(--bg-panel) !important; border-color: var(--border-light) !important; }
|
|
||||||
[data-theme="dark"] .hol-map-toggle { background: var(--bg-panel) !important; color: var(--text-muted) !important; border-color: var(--border-light) !important; }
|
|
||||||
[data-theme="dark"] .hol-map-toggle:hover { background: rgba(255,255,255,.06) !important; }
|
|
||||||
[data-theme="dark"] .hol-add-btn { background: var(--primary) !important; color: #fff !important; }
|
|
||||||
|
|
||||||
/* ─── GIFT-LIST MODULE ──────────────────────────────────────────────────────── */
|
[data-theme="dark"] .sum-selected {
|
||||||
[data-theme="dark"] .pf-gift-list { color: var(--text-main); }
|
background: rgba(59, 130, 246, 0.1) !important;
|
||||||
[data-theme="dark"] .pf-gift-list h1 { color: var(--text-main) !important; }
|
}
|
||||||
[data-theme="dark"] .cl-titlebar { background: var(--bg-panel) !important; border-color: var(--border-light) !important; }
|
[data-theme="dark"] .fc-burn-alert {
|
||||||
[data-theme="dark"] .cl-view-btn { background: var(--bg-panel) !important; color: var(--text-muted) !important; border-color: var(--border-light) !important; }
|
background: rgba(239, 68, 68, 0.1) !important;
|
||||||
[data-theme="dark"] .cl-view-btn.is-active { background: rgba(59,130,246,.15) !important; color: var(--primary) !important; border-color: rgba(59,130,246,.3) !important; }
|
color: #f87171 !important;
|
||||||
[data-theme="dark"] .pf-filter-bar { background: var(--bg-page) !important; border-color: var(--border-light) !important; }
|
}
|
||||||
[data-theme="dark"] .pf-filter-select { background: #1c2128 !important; color: var(--text-main) !important; border-color: var(--border-light) !important; }
|
[data-theme="dark"] .month-actions .btn-icon-small {
|
||||||
[data-theme="dark"] .pf-child-section { background: var(--bg-panel) !important; border-color: var(--border-light) !important; }
|
background: #1c2128 !important;
|
||||||
[data-theme="dark"] .pf-child-header { background: #1c2128 !important; border-color: var(--border-light) !important; }
|
border-color: var(--border-light) !important;
|
||||||
[data-theme="dark"] .pf-child-header h3 { color: var(--text-main) !important; }
|
color: var(--text-main) !important;
|
||||||
[data-theme="dark"] .pf-child-totals-bar { background: var(--bg-panel) !important; border-color: var(--border-light) !important; }
|
}
|
||||||
[data-theme="dark"] .pf-summary-pill { background: var(--bg-page) !important; border-color: var(--border-light) !important; color: var(--text-main) !important; }
|
[data-theme="dark"] .month-actions .btn-icon-small:hover {
|
||||||
[data-theme="dark"] .pf-gift-card-compact { background: var(--bg-panel) !important; border-color: var(--border-light) !important; }
|
background: rgba(255, 255, 255, 0.06) !important;
|
||||||
[data-theme="dark"] .pf-gift-card-compact:hover { border-color: var(--primary) !important; }
|
}
|
||||||
[data-theme="dark"] .pf-gift-title { color: var(--text-main) !important; }
|
|
||||||
[data-theme="dark"] .pf-gift-footer { border-color: var(--border-light) !important; }
|
|
||||||
@@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
is_admin TINYINT(1) DEFAULT 0,
|
is_admin TINYINT(1) DEFAULT 0,
|
||||||
is_active TINYINT(1) DEFAULT 1,
|
is_active TINYINT(1) DEFAULT 1,
|
||||||
lang VARCHAR(5) DEFAULT 'fr',
|
lang VARCHAR(5) DEFAULT 'fr',
|
||||||
|
remember_token VARCHAR(255) DEFAULT NULL, -- 👈 NOUVEAU CHAMP ICI
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (family_id) REFERENCES families(id)
|
FOREIGN KEY (family_id) REFERENCES families(id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
@@ -36,6 +37,7 @@ CREATE TABLE IF NOT EXISTS user_calendar_integrations (
|
|||||||
calendar_url VARCHAR(1024) DEFAULT NULL,
|
calendar_url VARCHAR(1024) DEFAULT NULL,
|
||||||
status VARCHAR(30) DEFAULT 'connected',
|
status VARCHAR(30) DEFAULT 'connected',
|
||||||
last_sync_at DATETIME DEFAULT NULL,
|
last_sync_at DATETIME DEFAULT NULL,
|
||||||
|
calendar_prefs_json TEXT DEFAULT NULL,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY uq_user_provider (user_id, provider),
|
UNIQUE KEY uq_user_provider (user_id, provider),
|
||||||
|
|||||||
+294
-116
@@ -1,10 +1,43 @@
|
|||||||
-- HouseHub — Schéma MySQL
|
-- HouseHub — Schéma MySQL (Modèle pour une base famille)
|
||||||
-- Créer la base : CREATE DATABASE househub CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
SET NAMES utf8mb4;
|
SET NAMES utf8mb4;
|
||||||
SET FOREIGN_KEY_CHECKS = 0;
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
-- ─── Utilisateurs ─────────────────────────────────────────────────────────────
|
-- ─── Configuration du foyer ───────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_foyer_settings (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
currency VARCHAR(10) NOT NULL DEFAULT '€',
|
||||||
|
zone_scolaire VARCHAR(5) NOT NULL DEFAULT 'C',
|
||||||
|
csv_mapping TEXT DEFAULT NULL,
|
||||||
|
cares_modes TEXT DEFAULT NULL,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '€', 'C');
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- Paramètres dynamiques des Modules (Key-Value)
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS `pf_settings` (
|
||||||
|
`setting_key` varchar(50) NOT NULL,
|
||||||
|
`setting_value` text DEFAULT NULL,
|
||||||
|
`module` varchar(50) NOT NULL,
|
||||||
|
`updated_at` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (`setting_key`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `pf_settings` (`setting_key`, `setting_value`, `module`) VALUES
|
||||||
|
('calendar_default_view', 'month', 'calendar'),
|
||||||
|
('calendar_first_day', '1', 'calendar'),
|
||||||
|
('calendar_working_hours', '08:00-19:00', 'calendar'),
|
||||||
|
('budget_start_day', '1', 'budget'),
|
||||||
|
('budget_default_tab', 'dépenses', 'budget'),
|
||||||
|
('travel_default_transport', 'car', 'voyage'),
|
||||||
|
('travel_default_fuel_price', '1.85', 'voyage'),
|
||||||
|
('gifts_hide_purchased', '0', 'cadeaux'),
|
||||||
|
('gifts_default_sort', 'person', 'cadeaux'),
|
||||||
|
('gifts_budget_alert', '500', 'cadeaux');
|
||||||
|
|
||||||
|
-- ─── Utilisateurs (Legacy) ────────────────────────────────────────────────────
|
||||||
CREATE TABLE IF NOT EXISTS pf_users (
|
CREATE TABLE IF NOT EXISTS pf_users (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
username VARCHAR(100) NOT NULL UNIQUE,
|
username VARCHAR(100) NOT NULL UNIQUE,
|
||||||
@@ -12,21 +45,16 @@ CREATE TABLE IF NOT EXISTS pf_users (
|
|||||||
display_name VARCHAR(100) NOT NULL
|
display_name VARCHAR(100) NOT NULL
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- ─── Familles ─────────────────────────────────────────────────────────────
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_foyer_settings (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
currency VARCHAR(10) NOT NULL DEFAULT '€',
|
|
||||||
zone_scolaire VARCHAR(5) NOT NULL DEFAULT 'C',
|
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Initialisation de la ligne unique par défaut
|
|
||||||
INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '€', 'C');
|
|
||||||
|
|
||||||
-- ─── Personnes ────────────────────────────────────────────────────────────────
|
-- ─── Personnes ────────────────────────────────────────────────────────────────
|
||||||
CREATE TABLE IF NOT EXISTS pf_people (
|
CREATE TABLE IF NOT EXISTS `pf_people` (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
name VARCHAR(100) NOT NULL
|
`name` varchar(100) NOT NULL,
|
||||||
|
`user_id` int(11) DEFAULT NULL,
|
||||||
|
`role` varchar(50) DEFAULT NULL,
|
||||||
|
`care_modes` varchar(255) DEFAULT NULL,
|
||||||
|
`color` varchar(7) DEFAULT '#0891b2',
|
||||||
|
`is_active` TINYINT(1) DEFAULT 1,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- ─── Calendrier familial ──────────────────────────────────────────────────────
|
-- ─── Calendrier familial ──────────────────────────────────────────────────────
|
||||||
@@ -60,14 +88,36 @@ CREATE TABLE IF NOT EXISTS pf_leave_snapshots (
|
|||||||
person_id INT NOT NULL,
|
person_id INT NOT NULL,
|
||||||
leave_type VARCHAR(50) NOT NULL,
|
leave_type VARCHAR(50) NOT NULL,
|
||||||
snapshot_date DATE NOT NULL,
|
snapshot_date DATE NOT NULL,
|
||||||
remaining_balance DECIMAL(6,2) DEFAULT 0
|
remaining_balance DECIMAL(6,2) DEFAULT 0.00,
|
||||||
|
carry_over_balance DECIMAL(6,2) DEFAULT 0.00
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_person_leave_meta (
|
CREATE TABLE IF NOT EXISTS pf_person_leave_meta (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT(11) AUTO_INCREMENT PRIMARY KEY,
|
||||||
person_id INT NOT NULL UNIQUE,
|
person_id INT(11) NOT NULL,
|
||||||
anniversary_date DATE
|
leave_type VARCHAR(50) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
method VARCHAR(20) COLLATE utf8mb4_unicode_ci DEFAULT 'FIXED',
|
||||||
|
allowance DECIMAL(5,2) DEFAULT 0.00,
|
||||||
|
carry_over_max_days DECIMAL(5,2) DEFAULT 0.00,
|
||||||
|
carry_over_deadline_month INT(11) DEFAULT 12,
|
||||||
|
anniversary_date DATE DEFAULT NULL,
|
||||||
|
INDEX (person_id),
|
||||||
|
INDEX (leave_type),
|
||||||
|
UNIQUE KEY uq_person_leave (person_id, leave_type)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ─── Types de Congés (Référentiel) ─────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_leave_types (
|
||||||
|
id INT(11) AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
code VARCHAR(20) COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
label VARCHAR(100) COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
default_allowance DECIMAL(5,2) DEFAULT 0.00,
|
||||||
|
reset_month INT(11) DEFAULT 1,
|
||||||
|
UNIQUE KEY code (code)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO pf_leave_types (code, label, default_allowance, reset_month) VALUES
|
||||||
|
('CP', 'Congés Payés', 25.00, 6);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_calendar_weeks (
|
CREATE TABLE IF NOT EXISTS pf_calendar_weeks (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@@ -84,83 +134,143 @@ CREATE TABLE IF NOT EXISTS pf_calendar_weeks (
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- ─── Budget ───────────────────────────────────────────────────────────────────
|
-- ─── Budget ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- NOUVEAU : Table des comptes bancaires
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_bank_accounts (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
account_type ENUM('checking', 'savings') DEFAULT 'checking',
|
||||||
|
owner_person_id INT DEFAULT NULL,
|
||||||
|
is_default TINYINT(1) DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (owner_person_id) REFERENCES pf_people(id) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_budget_items (
|
CREATE TABLE IF NOT EXISTS pf_budget_items (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
amount DECIMAL(10,2) DEFAULT 0,
|
amount DECIMAL(10,2) DEFAULT 0,
|
||||||
category VARCHAR(100),
|
category VARCHAR(100),
|
||||||
type VARCHAR(50),
|
type VARCHAR(50),
|
||||||
payment_day INT DEFAULT NULL,
|
payment_day INT DEFAULT NULL,
|
||||||
is_estimate TINYINT(1) DEFAULT 0,
|
is_estimate TINYINT(1) DEFAULT 0,
|
||||||
reg_month VARCHAR(7) DEFAULT NULL,
|
is_dynamic TINYINT(1) DEFAULT 0,
|
||||||
|
dynamic_code VARCHAR(50) DEFAULT NULL,
|
||||||
|
reg_month VARCHAR(7) DEFAULT NULL,
|
||||||
mapping_keywords TEXT DEFAULT NULL,
|
mapping_keywords TEXT DEFAULT NULL,
|
||||||
holiday_id INT DEFAULT NULL,
|
holiday_id INT DEFAULT NULL,
|
||||||
is_checked TINYINT(1) DEFAULT 0,
|
is_checked TINYINT(1) DEFAULT 0,
|
||||||
sort_order INT DEFAULT 0
|
sort_order INT DEFAULT 0
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_expenses (
|
CREATE TABLE IF NOT EXISTS pf_expenses (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
date_exp DATE NOT NULL,
|
date_exp DATE NOT NULL,
|
||||||
gestion_month VARCHAR(10) NOT NULL,
|
gestion_month VARCHAR(10) NOT NULL,
|
||||||
category VARCHAR(100),
|
category VARCHAR(100),
|
||||||
label VARCHAR(255),
|
label VARCHAR(255),
|
||||||
amount DECIMAL(10,2) DEFAULT 0,
|
amount DECIMAL(10,2) DEFAULT 0,
|
||||||
import_ref VARCHAR(255) DEFAULT NULL,
|
import_ref VARCHAR(255) DEFAULT NULL,
|
||||||
budget_item_id INT DEFAULT NULL,
|
budget_item_id INT DEFAULT NULL,
|
||||||
holiday_id INT DEFAULT NULL
|
holiday_id INT DEFAULT NULL,
|
||||||
|
salary_id INT DEFAULT NULL -- 🔥 AJOUT : Lien vers le salaire configuré
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `pf_expected_expenses` (
|
||||||
|
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`title` VARCHAR(150) NOT NULL,
|
||||||
|
`amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`expected_date` DATE NOT NULL,
|
||||||
|
`is_paid` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
INDEX `idx_expected_date` (`expected_date`),
|
||||||
|
INDEX `idx_is_paid` (`is_paid`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_alloc_categories (
|
CREATE TABLE IF NOT EXISTS pf_alloc_categories (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
name VARCHAR(100) NOT NULL,
|
name VARCHAR(100) NOT NULL,
|
||||||
target DECIMAL(10,2) DEFAULT 0,
|
target DECIMAL(10,2) DEFAULT 0.00,
|
||||||
|
transfer_dest VARCHAR(50) DEFAULT NULL,
|
||||||
holiday_id INT DEFAULT NULL,
|
holiday_id INT DEFAULT NULL,
|
||||||
sort_order INT DEFAULT 0
|
sort_order INT DEFAULT 0
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_alloc_values (
|
CREATE TABLE IF NOT EXISTS pf_alloc_values (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
month_date VARCHAR(10) NOT NULL,
|
month_date VARCHAR(10) NOT NULL,
|
||||||
cat_id INT NOT NULL,
|
cat_id INT NOT NULL,
|
||||||
amount_alex DECIMAL(10,2) DEFAULT 0,
|
person_id INT NOT NULL,
|
||||||
amount_laia DECIMAL(10,2) DEFAULT 0,
|
amount DECIMAL(10,2) DEFAULT 0.00,
|
||||||
UNIQUE KEY uq_alloc (month_date, cat_id)
|
UNIQUE KEY uq_alloc_person (month_date, cat_id, person_id),
|
||||||
|
FOREIGN KEY (person_id) REFERENCES pf_people(id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_savings (
|
CREATE TABLE IF NOT EXISTS pf_savings (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
owner VARCHAR(50) NOT NULL,
|
owner VARCHAR(50) NOT NULL,
|
||||||
month_date VARCHAR(10) NOT NULL,
|
month_date VARCHAR(10) NOT NULL,
|
||||||
category VARCHAR(100) NOT NULL,
|
category VARCHAR(100) NOT NULL,
|
||||||
amount DECIMAL(10,2) DEFAULT 0,
|
amount DECIMAL(10,2) DEFAULT 0,
|
||||||
holiday_id INT DEFAULT NULL
|
holiday_id INT DEFAULT NULL,
|
||||||
|
UNIQUE KEY uq_savings (owner, month_date, category)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_bank_snapshots (
|
CREATE TABLE IF NOT EXISTS pf_bank_snapshots (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
snapshot_date DATE NOT NULL,
|
snapshot_date DATE NOT NULL,
|
||||||
amount DECIMAL(12,2) DEFAULT 0
|
amount DECIMAL(12,2) DEFAULT 0
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_salary_config (
|
CREATE TABLE IF NOT EXISTS pf_salary_config (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
year INT NOT NULL,
|
year INT NOT NULL,
|
||||||
person VARCHAR(50) NOT NULL,
|
person VARCHAR(50) NOT NULL,
|
||||||
salary DECIMAL(10,2) DEFAULT 0,
|
salary DECIMAL(10,2) DEFAULT 0,
|
||||||
mensualite DECIMAL(10,2) DEFAULT 0,
|
mensualite DECIMAL(10,2) DEFAULT 0,
|
||||||
frais_func DECIMAL(10,2) DEFAULT 0,
|
frais_func DECIMAL(10,2) DEFAULT 0,
|
||||||
eco_perso DECIMAL(10,2) DEFAULT 0,
|
eco_perso DECIMAL(10,2) DEFAULT 0,
|
||||||
eco_family DECIMAL(10,2) DEFAULT 0,
|
eco_family DECIMAL(10,2) DEFAULT 0,
|
||||||
UNIQUE KEY uq_salary (year, person)
|
UNIQUE KEY uq_salary (year, person)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_import_rules (
|
CREATE TABLE IF NOT EXISTS pf_import_rules (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
keyword VARCHAR(255) NOT NULL UNIQUE,
|
keyword VARCHAR(255) NOT NULL UNIQUE,
|
||||||
category VARCHAR(100) NOT NULL
|
category VARCHAR(100) NOT NULL,
|
||||||
|
budget_item_id INT(11) NULL DEFAULT NULL
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `pf_advances` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`advance_date` date NOT NULL,
|
||||||
|
`payer` varchar(50) NOT NULL,
|
||||||
|
`description` varchar(255) NOT NULL,
|
||||||
|
`amount` decimal(10,2) DEFAULT 0.00,
|
||||||
|
`from_savings` tinyint(1) DEFAULT 0,
|
||||||
|
`is_resolved` tinyint(1) DEFAULT 0,
|
||||||
|
`created_at` datetime DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ─── Budget Catégories (Dynamiques) ─────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_budget_categories (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
code VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
label VARCHAR(100) NOT NULL,
|
||||||
|
type VARCHAR(50) NOT NULL,
|
||||||
|
color VARCHAR(20) DEFAULT '#ccc',
|
||||||
|
icon VARCHAR(20) DEFAULT '?'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
-- Insertion des catégories de base pour toute nouvelle famille
|
||||||
|
INSERT IGNORE INTO pf_budget_categories (code, label, type, color, icon) VALUES
|
||||||
|
('INCOME', 'Revenus', 'Income', '#10b981', '💵'),
|
||||||
|
('FIXED', 'Charges Fixes', 'Expense', '#ef4444', '🏢');
|
||||||
|
|
||||||
|
-- ─── Notes / Memo ─────────────────────────────────────────────────────────────
|
||||||
CREATE TABLE IF NOT EXISTS pf_notes (
|
CREATE TABLE IF NOT EXISTS pf_notes (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
note_type VARCHAR(100) NOT NULL,
|
note_type VARCHAR(100) NOT NULL,
|
||||||
@@ -169,6 +279,29 @@ CREATE TABLE IF NOT EXISTS pf_notes (
|
|||||||
UNIQUE KEY uq_note (note_type, reference_id)
|
UNIQUE KEY uq_note (note_type, reference_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_memo_notes (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
content LONGTEXT DEFAULT '',
|
||||||
|
tags VARCHAR(1000) DEFAULT '',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FULLTEXT KEY ft_notes (title, content, tags)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_memo_attachments (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
note_id INT NOT NULL,
|
||||||
|
type ENUM('image','file','url') NOT NULL DEFAULT 'file',
|
||||||
|
filename VARCHAR(255) DEFAULT NULL,
|
||||||
|
original_name VARCHAR(255) DEFAULT NULL,
|
||||||
|
url TEXT DEFAULT NULL,
|
||||||
|
label VARCHAR(255) DEFAULT NULL,
|
||||||
|
size INT DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (note_id) REFERENCES pf_memo_notes(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- ─── Voyages / Holidays ───────────────────────────────────────────────────────
|
-- ─── Voyages / Holidays ───────────────────────────────────────────────────────
|
||||||
CREATE TABLE IF NOT EXISTS pf_holidays (
|
CREATE TABLE IF NOT EXISTS pf_holidays (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@@ -179,7 +312,33 @@ CREATE TABLE IF NOT EXISTS pf_holidays (
|
|||||||
status VARCHAR(50) DEFAULT 'draft',
|
status VARCHAR(50) DEFAULT 'draft',
|
||||||
budget_food DECIMAL(10,2) DEFAULT 0,
|
budget_food DECIMAL(10,2) DEFAULT 0,
|
||||||
budget_extra DECIMAL(10,2) DEFAULT 0,
|
budget_extra DECIMAL(10,2) DEFAULT 0,
|
||||||
notes TEXT DEFAULT NULL
|
notes TEXT DEFAULT NULL,
|
||||||
|
vehicle_id INT DEFAULT NULL,
|
||||||
|
return_step_id INT DEFAULT NULL,
|
||||||
|
image_url VARCHAR(500) DEFAULT NULL,
|
||||||
|
FOREIGN KEY (vehicle_id) REFERENCES pf_vehicles(id) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_holidays_items (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
holiday_id INT NOT NULL,
|
||||||
|
category VARCHAR(100),
|
||||||
|
name VARCHAR(255),
|
||||||
|
amount DECIMAL(10,2) DEFAULT 0,
|
||||||
|
is_paid TINYINT(1) DEFAULT 0,
|
||||||
|
location_name VARCHAR(255) DEFAULT NULL,
|
||||||
|
lat DECIMAL(10,7) DEFAULT NULL,
|
||||||
|
lng DECIMAL(10,7) DEFAULT NULL,
|
||||||
|
sort_order INT DEFAULT 0,
|
||||||
|
notes TEXT DEFAULT NULL,
|
||||||
|
item_date DATE DEFAULT NULL,
|
||||||
|
item_time TIME DEFAULT NULL,
|
||||||
|
step_type VARCHAR(20) DEFAULT 'stop',
|
||||||
|
step_start_date DATE DEFAULT NULL,
|
||||||
|
step_end_date DATE DEFAULT NULL,
|
||||||
|
duration INT DEFAULT NULL,
|
||||||
|
expense_context VARCHAR(20) DEFAULT NULL,
|
||||||
|
FOREIGN KEY (holiday_id) REFERENCES pf_holidays(id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_holidays_items (
|
CREATE TABLE IF NOT EXISTS pf_holidays_items (
|
||||||
@@ -214,6 +373,17 @@ CREATE TABLE IF NOT EXISTS pf_holidays_ideas (
|
|||||||
notes TEXT DEFAULT NULL
|
notes TEXT DEFAULT NULL
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `pf_holidays_attachments` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`holiday_id` INT NOT NULL,
|
||||||
|
`item_id` INT DEFAULT NULL,
|
||||||
|
`file_name` VARCHAR(255) NOT NULL,
|
||||||
|
`file_path` VARCHAR(255) NOT NULL,
|
||||||
|
`uploaded_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
CONSTRAINT `fk_holiday_attachment` FOREIGN KEY (`holiday_id`) REFERENCES `pf_holidays`(`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_geocode_cache (
|
CREATE TABLE IF NOT EXISTS pf_geocode_cache (
|
||||||
q_hash CHAR(64) PRIMARY KEY,
|
q_hash CHAR(64) PRIMARY KEY,
|
||||||
q VARCHAR(255),
|
q VARCHAR(255),
|
||||||
@@ -225,27 +395,42 @@ CREATE TABLE IF NOT EXISTS pf_geocode_cache (
|
|||||||
|
|
||||||
-- ─── Cadeaux ──────────────────────────────────────────────────────────────────
|
-- ─── Cadeaux ──────────────────────────────────────────────────────────────────
|
||||||
CREATE TABLE IF NOT EXISTS pf_gifts (
|
CREATE TABLE IF NOT EXISTS pf_gifts (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT(11) AUTO_INCREMENT PRIMARY KEY,
|
||||||
year INT NOT NULL,
|
year INT(11) NOT NULL,
|
||||||
adult_name VARCHAR(100) NOT NULL,
|
adult_name VARCHAR(100) NOT NULL,
|
||||||
payer_name VARCHAR(100),
|
payer_name VARCHAR(100) DEFAULT NULL,
|
||||||
child_name VARCHAR(100) NOT NULL,
|
child_name VARCHAR(100) NOT NULL,
|
||||||
occasion VARCHAR(50) NOT NULL,
|
occasion VARCHAR(50) NOT NULL,
|
||||||
gift_description TEXT NOT NULL,
|
gift_description TEXT NOT NULL,
|
||||||
product_link VARCHAR(500) DEFAULT NULL,
|
product_link VARCHAR(500) DEFAULT NULL,
|
||||||
amount DECIMAL(8,2) DEFAULT 0
|
amount DECIMAL(8,2) DEFAULT 0.00
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
CREATE TABLE IF NOT EXISTS pf_gift_occasions (
|
||||||
|
id INT(11) AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
code VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
icon VARCHAR(20) DEFAULT '🎁',
|
||||||
|
month_date VARCHAR(5) DEFAULT NULL,
|
||||||
|
is_active TINYINT(1) DEFAULT 1,
|
||||||
|
sort_order INT DEFAULT 0
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
-- ─── Données initiales ────────────────────────────────────────────────────────
|
INSERT IGNORE INTO pf_gift_occasions (code, name, icon, month_date, is_active, sort_order) VALUES
|
||||||
-- Utilisateur admin (mot de passe : changeme → à modifier !)
|
('XMAS', 'Noël', '🎄', '12-25', 1, 1),
|
||||||
INSERT IGNORE INTO pf_users (id, username, password_hash, display_name)
|
('BIRTHDAY', 'Anniversaire', '🎂', NULL, 1, 2),
|
||||||
VALUES (1, 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin');
|
('OTHER', 'Autre', '🎁', NULL, 1, 99);
|
||||||
|
|
||||||
-- Personnes (IDs fixes correspondant aux constantes dans config.php)
|
-- Table des règles (Qui offre à Qui)
|
||||||
INSERT IGNORE INTO pf_people (id, name) VALUES (2, 'Alex');
|
CREATE TABLE IF NOT EXISTS pf_gift_rules (
|
||||||
INSERT IGNORE INTO pf_people (id, name) VALUES (3, 'Laia');
|
id INT(11) AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
adult_person_id INT(11) NOT NULL,
|
||||||
|
child_person_id INT(11) NOT NULL,
|
||||||
|
occasion_id INT(11) NOT NULL,
|
||||||
|
INDEX (adult_person_id),
|
||||||
|
INDEX (child_person_id),
|
||||||
|
INDEX (occasion_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
-- ─── Garage Manager ───────────────────────────────────────────────────────────
|
-- ─── Garage Manager ───────────────────────────────────────────────────────────
|
||||||
CREATE TABLE IF NOT EXISTS pf_vehicles (
|
CREATE TABLE IF NOT EXISTS pf_vehicles (
|
||||||
@@ -257,6 +442,7 @@ CREATE TABLE IF NOT EXISTS pf_vehicles (
|
|||||||
license_plate VARCHAR(50) DEFAULT NULL,
|
license_plate VARCHAR(50) DEFAULT NULL,
|
||||||
vin VARCHAR(100) DEFAULT NULL,
|
vin VARCHAR(100) DEFAULT NULL,
|
||||||
fuel_type VARCHAR(50) DEFAULT 'Essence',
|
fuel_type VARCHAR(50) DEFAULT 'Essence',
|
||||||
|
consumption decimal(4,2) DEFAULT NULL COMMENT 'Consommation L/100km',
|
||||||
color VARCHAR(50) DEFAULT NULL,
|
color VARCHAR(50) DEFAULT NULL,
|
||||||
purchase_date DATE DEFAULT NULL,
|
purchase_date DATE DEFAULT NULL,
|
||||||
purchase_price DECIMAL(10,2) DEFAULT NULL,
|
purchase_price DECIMAL(10,2) DEFAULT NULL,
|
||||||
@@ -321,30 +507,6 @@ CREATE TABLE IF NOT EXISTS pf_garage_documents (
|
|||||||
FOREIGN KEY (maintenance_id) REFERENCES pf_maintenances(id) ON DELETE CASCADE
|
FOREIGN KEY (maintenance_id) REFERENCES pf_maintenances(id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- ─── Notes / Memo ─────────────────────────────────────────────────────────────
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_memo_notes (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
title VARCHAR(255) NOT NULL,
|
|
||||||
content LONGTEXT DEFAULT '',
|
|
||||||
tags VARCHAR(1000) DEFAULT '',
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
FULLTEXT KEY ft_notes (title, content, tags)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_memo_attachments (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
note_id INT NOT NULL,
|
|
||||||
type ENUM('image','file','url') NOT NULL DEFAULT 'file',
|
|
||||||
filename VARCHAR(255) DEFAULT NULL,
|
|
||||||
original_name VARCHAR(255) DEFAULT NULL,
|
|
||||||
url TEXT DEFAULT NULL,
|
|
||||||
label VARCHAR(255) DEFAULT NULL,
|
|
||||||
size INT DEFAULT 0,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (note_id) REFERENCES pf_memo_notes(id) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- ─── Todo ─────────────────────────────────────────────────────────────────────
|
-- ─── Todo ─────────────────────────────────────────────────────────────────────
|
||||||
CREATE TABLE IF NOT EXISTS pf_todo_lists (
|
CREATE TABLE IF NOT EXISTS pf_todo_lists (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@@ -372,15 +534,17 @@ CREATE TABLE IF NOT EXISTS pf_todos (
|
|||||||
FOREIGN KEY (list_id) REFERENCES pf_todo_lists(id) ON DELETE SET NULL
|
FOREIGN KEY (list_id) REFERENCES pf_todo_lists(id) ON DELETE SET NULL
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- ─── Module Liste (multi-listes) ──────────────────────────────────────────────
|
-- ─── Module Liste (Courses partagées) ─────────────────────────────────────────
|
||||||
CREATE TABLE IF NOT EXISTS pf_lists (
|
CREATE TABLE IF NOT EXISTS pf_lists (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL DEFAULT 'Ma liste',
|
name VARCHAR(255) NOT NULL DEFAULT 'Ma liste',
|
||||||
position INT NOT NULL DEFAULT 0,
|
color VARCHAR(20) DEFAULT NULL,
|
||||||
|
list_type VARCHAR(50) DEFAULT NULL,
|
||||||
|
position INT NOT NULL DEFAULT 0,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
INSERT INTO pf_lists (id, name, position) VALUES (1, 'Ma liste', 0);
|
INSERT IGNORE INTO pf_lists (id, name, position) VALUES (1, 'Ma liste', 0);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_grocery_items (
|
CREATE TABLE IF NOT EXISTS pf_grocery_items (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@@ -445,6 +609,7 @@ CREATE TABLE IF NOT EXISTS pf_calendar_event_links (
|
|||||||
external_uid VARCHAR(255) NOT NULL,
|
external_uid VARCHAR(255) NOT NULL,
|
||||||
external_etag VARCHAR(255) DEFAULT NULL,
|
external_etag VARCHAR(255) DEFAULT NULL,
|
||||||
calendar_url VARCHAR(1024) DEFAULT NULL,
|
calendar_url VARCHAR(1024) DEFAULT NULL,
|
||||||
|
external_href VARCHAR(2048) DEFAULT NULL,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY uq_calendar_event (calendar_event_id),
|
UNIQUE KEY uq_calendar_event (calendar_event_id),
|
||||||
@@ -496,3 +661,16 @@ CREATE TABLE IF NOT EXISTS pf_planka_config (
|
|||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (id)
|
PRIMARY KEY (id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
|
-- ─── Planificateur de Repas ───────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_meals_plan (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
plan_date DATE NOT NULL,
|
||||||
|
service ENUM('lunch', 'dinner') NOT NULL,
|
||||||
|
person_id INT NOT NULL,
|
||||||
|
meal_name VARCHAR(500) DEFAULT NULL,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uq_meal_plan (plan_date, service, person_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* HouseHub OS - Source Extractor 🦙🚀
|
|
||||||
* Génère un fichier Markdown contenant tout le code source du projet.
|
|
||||||
* Idéal pour alimenter une GEM ou un LLM.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 1. 🚨 SÉCURITÉ : Ne jamais laisser ce fichier accessible publiquement sans protection !
|
|
||||||
// Appelle le script via : https://househub.nas.../extract_source.php?key=TON_MOT_DE_PASSE
|
|
||||||
$secretKey = 'mR83s7MmXbP$jer$9C4HnGkry6xhGL6';
|
|
||||||
|
|
||||||
if (!isset($_GET['key']) || $_GET['key'] !== $secretKey) {
|
|
||||||
http_response_code(403);
|
|
||||||
die("⛔ Accès refusé. Veuillez fournir la clé de sécurité.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forcer l'affichage en texte brut dans le navigateur pour un copier-coller facile
|
|
||||||
header('Content-Type: text/plain; charset=utf-8');
|
|
||||||
|
|
||||||
$root = __DIR__;
|
|
||||||
|
|
||||||
// 2. CONFIGURATION DU FILTRAGE
|
|
||||||
// Dossiers à exclure (ne mets pas de slash au début)
|
|
||||||
$excludeDirs = [
|
|
||||||
'.git',
|
|
||||||
'.gitea',
|
|
||||||
'.devtools',
|
|
||||||
'vendor',
|
|
||||||
'node_modules',
|
|
||||||
'assets/img',
|
|
||||||
'modules/holidays/assets/img',
|
|
||||||
'modules/family-calendar/assets/img',
|
|
||||||
'modules/gift-list/assets/img'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Fichiers spécifiques à exclure (Sécurité & Bruit)
|
|
||||||
$excludeFiles = [
|
|
||||||
'.env',
|
|
||||||
'.env.example',
|
|
||||||
'extract_source.php', // On s'exclut soi-même
|
|
||||||
'pachafamily_source.md',
|
|
||||||
'docker-compose.override.yml'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Extensions autorisées (on ignore les images, pdf, zip, etc.)
|
|
||||||
$allowedExtensions = ['php', 'js', 'css', 'sql', 'json', 'yml', 'yaml', 'md', 'sh'];
|
|
||||||
|
|
||||||
// 3. INITIALISATION DU PARCOURS
|
|
||||||
$iterator = new RecursiveIteratorIterator(
|
|
||||||
new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS),
|
|
||||||
RecursiveIteratorIterator::SELF_FIRST
|
|
||||||
);
|
|
||||||
|
|
||||||
$report = "# 🦙 Source Code HouseHub OS\n\n";
|
|
||||||
$report .= "> *Généré le " . date('Y-m-d H:i:s') . "*\n\n";
|
|
||||||
$report .= "---\n\n";
|
|
||||||
|
|
||||||
// 4. BOUCLE D'EXTRACTION
|
|
||||||
foreach ($iterator as $file) {
|
|
||||||
if ($file->isDir()) continue;
|
|
||||||
|
|
||||||
$filePath = $file->getPathname();
|
|
||||||
$relativePath = str_replace($root . DIRECTORY_SEPARATOR, '', $filePath);
|
|
||||||
$relativePath = str_replace('\\', '/', $relativePath); // Uniformisation Windows/Linux
|
|
||||||
|
|
||||||
// Extraction de l'extension
|
|
||||||
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
|
||||||
|
|
||||||
// A. Filtrage par dossier
|
|
||||||
$skip = false;
|
|
||||||
foreach ($excludeDirs as $dir) {
|
|
||||||
// Si le chemin commence par ce dossier, ou contient /ce_dossier/
|
|
||||||
if (strpos($relativePath, $dir . '/') === 0 || strpos($relativePath, '/' . $dir . '/') !== false) {
|
|
||||||
$skip = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($skip) continue;
|
|
||||||
|
|
||||||
// B. Filtrage par nom de fichier
|
|
||||||
if (in_array(basename($filePath), $excludeFiles)) continue;
|
|
||||||
|
|
||||||
// C. Filtrage par extension
|
|
||||||
if (!in_array($ext, $allowedExtensions)) continue;
|
|
||||||
|
|
||||||
// 5. LECTURE ET FORMATAGE
|
|
||||||
$content = file_get_contents($filePath);
|
|
||||||
|
|
||||||
if ($content === false) {
|
|
||||||
$content = "// Erreur lors de la lecture de ce fichier.";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ajustement du tag de langage pour le Markdown
|
|
||||||
$lang = $ext;
|
|
||||||
if ($ext === 'js') $lang = 'javascript';
|
|
||||||
if ($ext === 'yml') $lang = 'yaml';
|
|
||||||
|
|
||||||
$report .= "### 📄 Fichier : `$relativePath`\n";
|
|
||||||
$report .= "```$lang\n";
|
|
||||||
$report .= trim($content) . "\n";
|
|
||||||
$report .= "```\n\n---\n\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. SORTIE
|
|
||||||
echo $report;
|
|
||||||
+426
-139
@@ -8,6 +8,75 @@ require_login();
|
|||||||
require __DIR__ . '/includes/db.php';
|
require __DIR__ . '/includes/db.php';
|
||||||
require_once __DIR__ . '/includes/i18n.php';
|
require_once __DIR__ . '/includes/i18n.php';
|
||||||
|
|
||||||
|
// 1. Récupération de toutes les personnes
|
||||||
|
$stmtPeople = $pdo->query("SELECT id, name, user_id, role, color, care_modes FROM pf_people WHERE is_active = 1 ORDER BY id ASC");
|
||||||
|
$familyPeople = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$parents = [];
|
||||||
|
$kids = [];
|
||||||
|
$helpers = [];
|
||||||
|
|
||||||
|
foreach ($familyPeople as $p) {
|
||||||
|
$role = strtolower($p['role'] ?? '');
|
||||||
|
if ($role === 'parent') {
|
||||||
|
$parents[] = $p;
|
||||||
|
} elseif ($role === 'enfant' || $role === 'child') {
|
||||||
|
$p['modes'] = json_decode($p['care_modes'] ?? '[]', true) ?: [];
|
||||||
|
$kids[] = $p;
|
||||||
|
} elseif ($role === 'helper' || $role === 'nounou') {
|
||||||
|
$helpers[] = $p; // 🟢 On stocke les intervenants
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Modes de garde (globaux du foyer, pour synchroniser avec le JS)
|
||||||
|
$stmtFoyer = $pdo->query("SELECT care_modes, zone_scolaire FROM pf_foyer_settings LIMIT 1");
|
||||||
|
$foyerData = $stmtFoyer->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$activeCareModes = json_decode($foyerData['care_modes'] ?? '[]', true);
|
||||||
|
if (!is_array($activeCareModes)) $activeCareModes = [];
|
||||||
|
|
||||||
|
// 3. Matrice de congés dynamique (pour les en-têtes et la modale)
|
||||||
|
$stmtLeaves = $pdo->query("SELECT person_id, leave_type FROM pf_person_leave_meta ORDER BY id ASC");
|
||||||
|
$dbLeaves = $stmtLeaves->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
$leaveMatrix = [];
|
||||||
|
$allLeaveTypes = [];
|
||||||
|
foreach ($dbLeaves as $l) {
|
||||||
|
$leaveMatrix[$l['person_id']][] = $l['leave_type'];
|
||||||
|
$allLeaveTypes[$l['leave_type']] = true;
|
||||||
|
}
|
||||||
|
$allLeaveTypes = array_keys($allLeaveTypes);
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// VÉRIFICATIONS D'INTÉGRITÉ (ALERTES)
|
||||||
|
// ==========================================
|
||||||
|
$calendarAlerts = [];
|
||||||
|
|
||||||
|
// 1. Aucun membre adulte/parent configuré
|
||||||
|
if (empty($parents)) {
|
||||||
|
$calendarAlerts[] = "<strong>Aucun adulte configuré.</strong> Rendez-vous dans les Paramètres Généraux de HouseHub pour ajouter les membres du foyer.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Enfants présents, mais aucun mode de garde global défini
|
||||||
|
if (!empty($kids) && empty($activeCareModes)) {
|
||||||
|
$calendarAlerts[] = "<strong>Modes de garde manquants.</strong> Vous avez des enfants, mais aucun mode de garde n'est défini. Cliquez sur l'icône ⚙️ (Paramètres du calendrier) pour les ajouter.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Référentiel des types de congés vide
|
||||||
|
try {
|
||||||
|
$countLeaveTypes = $pdo->query("SELECT COUNT(*) FROM pf_leave_types")->fetchColumn();
|
||||||
|
if ($countLeaveTypes == 0) {
|
||||||
|
$calendarAlerts[] = "<strong>Types de congés manquants.</strong> Le catalogue des congés (CP, RTT...) est vide. Ajoutez-les dans les paramètres généraux.";
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Si la table n'existe pas encore
|
||||||
|
$calendarAlerts[] = "<strong>Table des congés manquante.</strong> Veuillez mettre à jour la base de données.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Zone scolaire manquante (si tu l'as stockée dans pf_foyer_settings)
|
||||||
|
$zoneScolaire = $foyerData['zone_scolaire'] ?? null;
|
||||||
|
if (empty($zoneScolaire)) {
|
||||||
|
$calendarAlerts[] = "<strong>Zone scolaire non définie.</strong> Cliquez sur l'icône ⚙️ pour configurer votre zone afin d'afficher les vacances scolaires.";
|
||||||
|
}
|
||||||
|
|
||||||
$pageTitle = tr('fc_page_title');
|
$pageTitle = tr('fc_page_title');
|
||||||
$activePage = "family-calendar";
|
$activePage = "family-calendar";
|
||||||
$mainClass = "pf-family-calendar";
|
$mainClass = "pf-family-calendar";
|
||||||
@@ -16,11 +85,19 @@ $pageCss = "/modules/family-calendar/family-calendar.css";
|
|||||||
require __DIR__ . '/header.php';
|
require __DIR__ . '/header.php';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="pf-container" style="max-width:100%; padding:0;">
|
<div class="pf-container" style="--p1-main: <?= $parents[0]['color'] ?? '#0891b2' ?>; --p2-main: <?= $parents[1]['color'] ?? '#f59e0b' ?>; max-width:100%; padding:0;">
|
||||||
<div class="fc-header-row">
|
<div class="fc-header-row" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px; margin-bottom: 24px;">
|
||||||
<h1><?= tr('fc_main_header') ?></h1>
|
|
||||||
|
|
||||||
<div style="display: flex; gap: 10px;">
|
<!-- Bloc Titre + Roue crantée -->
|
||||||
|
<div style="display: flex; align-items: center; gap: 12px;">
|
||||||
|
<h1 style="margin: 0;"><?= tr('fc_main_header') ?></h1>
|
||||||
|
<button id="btnOpenCalendarSettings" class="btn-settings-gear" title="Paramètres du calendrier" style="font-size: 1.4rem; padding: 4px;">
|
||||||
|
⚙️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Boutons d'actions -->
|
||||||
|
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||||
<button id="btnOpenSnapshotModal" class="pf-btn-icon-text">
|
<button id="btnOpenSnapshotModal" class="pf-btn-icon-text">
|
||||||
<span class="icon">⚖️</span>
|
<span class="icon">⚖️</span>
|
||||||
<span class="text"><?= tr('fc_btn_correct_balances') ?></span>
|
<span class="text"><?= tr('fc_btn_correct_balances') ?></span>
|
||||||
@@ -32,6 +109,20 @@ require __DIR__ . '/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($calendarAlerts)): ?>
|
||||||
|
<div class="pf-alert pf-alert-warning" style="background: var(--bg-warning, #fffbeb); border: 1px solid var(--border-warning, #fcd34d); color: var(--text-warning, #92400e); padding: 16px; border-radius: 8px; margin-bottom: 24px; display: flex; gap: 12px; align-items: flex-start;">
|
||||||
|
<div style="font-size: 1.5rem; line-height: 1;">⚠️</div>
|
||||||
|
<div>
|
||||||
|
<h4 style="margin: 0 0 8px 0; font-size: 1.05rem;">Configuration incomplète</h4>
|
||||||
|
<ul style="margin: 0; padding-left: 18px; font-size: 0.95rem; line-height: 1.5;">
|
||||||
|
<?php foreach ($calendarAlerts as $alert): ?>
|
||||||
|
<li style="margin-bottom: 4px;"><?= $alert ?></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div id="modalHolidays" class="pf-modal">
|
<div id="modalHolidays" class="pf-modal">
|
||||||
<div class="pf-modal-content">
|
<div class="pf-modal-content">
|
||||||
<div class="pf-modal-header">
|
<div class="pf-modal-header">
|
||||||
@@ -70,16 +161,20 @@ require __DIR__ . '/header.php';
|
|||||||
<div class="pf-form-group">
|
<div class="pf-form-group">
|
||||||
<label class="pf-label"><?= tr('fc_label_person') ?></label>
|
<label class="pf-label"><?= tr('fc_label_person') ?></label>
|
||||||
<select id="snapPerson" class="pf-input" required>
|
<select id="snapPerson" class="pf-input" required>
|
||||||
<option value="2">Alex</option>
|
<option value="" disabled selected>-- <?= tr('fc_choose_person') ?> --</option>
|
||||||
<option value="3">Laia</option>
|
<?php foreach ($parents as $person): ?>
|
||||||
|
<option value="<?= htmlspecialchars($person['id']) ?>">
|
||||||
|
<?= htmlspecialchars($person['name']) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-form-group">
|
<div class="pf-form-group">
|
||||||
<label class="pf-label"><?= tr('fc_label_leave_type') ?></label>
|
<label class="pf-label"><?= tr('fc_label_leave_type') ?></label>
|
||||||
<select id="snapType" class="pf-input" required>
|
<select id="snapType" class="pf-input" required>
|
||||||
<option value="CP">CP</option>
|
<?php foreach ($allLeaveTypes as $type): ?>
|
||||||
<option value="JRA">JRA</option>
|
<option value="<?= htmlspecialchars($type) ?>"><?= htmlspecialchars($type) ?></option>
|
||||||
<option value="JA">JA</option>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,172 +185,364 @@ require __DIR__ . '/header.php';
|
|||||||
<div class="pf-form-group">
|
<div class="pf-form-group">
|
||||||
<label class="pf-label"><?= tr('fc_label_remaining_balance') ?></label>
|
<label class="pf-label"><?= tr('fc_label_remaining_balance') ?></label>
|
||||||
<div style="display:flex; align-items:center;">
|
<div style="display:flex; align-items:center;">
|
||||||
<input type="number" step="0.5" id="snapBalance" class="pf-input" placeholder="<?= tr('fc_placeholder_balance') ?>" style="border-radius: 8px 0 0 8px; border-right: none; font-weight: bold; color: var(--pf-primary);" required>
|
<!-- 🔥 STEP 0.01 POUR DEUX DÉCIMALES STRICTES -->
|
||||||
|
<input type="number" step="0.01" id="snapBalance" class="pf-input" placeholder="<?= tr('fc_placeholder_balance') ?>" style="border-radius: 8px 0 0 8px; border-right: none; font-weight: bold; color: var(--pf-primary);" required>
|
||||||
<span style="background: var(--pf-bg-lighter); border: 1px solid var(--pf-border); padding: 0 12px; height: 42px; display: flex; align-items: center; border-radius: 0 8px 8px 0; color: var(--pf-text-muted); font-size: 0.9rem; font-weight: 600;">
|
<span style="background: var(--pf-bg-lighter); border: 1px solid var(--pf-border); padding: 0 12px; height: 42px; display: flex; align-items: center; border-radius: 0 8px 8px 0; color: var(--pf-text-muted); font-size: 0.9rem; font-weight: 600;">
|
||||||
<?= tr('fc_unit_days') ?>
|
<?= tr('fc_unit_days') ?>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- NOUVEAU BLOC : Solde reporté -->
|
||||||
|
<div class="pf-form-group" style="margin-top: 15px;">
|
||||||
|
<label class="pf-label"><?= tr('fc_label_carry_over') ?></label>
|
||||||
|
<div style="display:flex; align-items:center;">
|
||||||
|
<!-- 🔥 STEP 0.01 POUR DEUX DÉCIMALES STRICTES -->
|
||||||
|
<input type="number" step="0.01" id="snapCarryOver" class="pf-input" placeholder="ex: 2.00" value="0" style="border-radius: 8px 0 0 8px; border-right: none; font-weight: bold; color: var(--danger);" required>
|
||||||
|
<span style="background: var(--pf-bg-lighter); border: 1px solid var(--pf-border); padding: 0 12px; height: 42px; display: flex; align-items: center; border-radius: 0 8px 8px 0; color: var(--pf-text-muted); font-size: 0.9rem; font-weight: 600;">
|
||||||
|
<?= tr('fc_unit_days') ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<small class="pf-muted-note"><?= tr('fc_carry_over_note') ?></small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FOOTER INTÉGRÉ DANS LE FORMULAIRE POUR ÉVITER LE BUG DE CIBLAGE -->
|
||||||
|
<div class="pf-modal-footer" style="margin-top: 20px; padding-top: 15px; border-top: 1px solid var(--pf-border); display: flex; justify-content: flex-end; gap: 10px;">
|
||||||
|
<button type="button" class="pf-btn pf-btn-secondary" onclick="document.getElementById('modalSnapshot').style.display = 'none'; document.body.classList.remove('no-scroll');"><?= tr('btn_cancel') ?></button>
|
||||||
|
<button type="submit" id="btnSubmitSnapshot" class="pf-btn pf-btn-primary"><?= tr('fc_btn_save_snap') ?></button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-modal-footer">
|
</div>
|
||||||
<button type="button" class="pf-btn pf-btn-secondary" onclick="document.getElementById('modalSnapshot').classList.remove('open'); document.body.classList.remove('no-scroll');"><?= tr('btn_cancel') ?></button>
|
</div>
|
||||||
<button type="submit" form="formSnapshot" class="pf-btn pf-btn-primary"><?= tr('fc_btn_save_snap') ?></button>
|
|
||||||
|
<div id="modalCalendarSettings" class="pf-modal">
|
||||||
|
<div class="pf-modal-content" style="max-width: 650px; width: 95%;">
|
||||||
|
|
||||||
|
<div class="pf-modal-header">
|
||||||
|
<h3 class="pf-modal-title">⚙️ <?= tr('fc_modal_settings_title') ?></h3>
|
||||||
|
<button type="button" class="pf-modal-close" onclick="closeCalendarSettings()">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 4px; background: var(--bg-page); padding: 4px; border-radius: 8px; margin: 1rem;">
|
||||||
|
<button type="button" id="tab-btn-foyer" class="bs-tab-btn active" style="flex: 1; padding: 8px 12px; font-weight: 600;" onclick="switchCalendarTab('foyer')">
|
||||||
|
🏡 <?= tr('fc_tab_foyer') ?>
|
||||||
|
</button>
|
||||||
|
<button type="button" id="tab-btn-membres" class="bs-tab-btn" style="flex: 1; padding: 8px 12px; font-weight: 600;" onclick="switchCalendarTab('membres')">
|
||||||
|
👥 <?= tr('fc_tab_members') ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pf-modal-body" style="max-height: 65vh; overflow-y: auto; padding-top: 0; padding-bottom: 1rem;">
|
||||||
|
|
||||||
|
<div id="cal-pane-foyer" class="cal-settings-pane">
|
||||||
|
|
||||||
|
<form id="formCalFoyer" onsubmit="submitCalFoyer(event)">
|
||||||
|
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label"><?= tr('fc_zone_label') ?></label>
|
||||||
|
<select id="setZoneScolaire" name="zone_scolaire" class="pf-input" required style="width: 100%;">
|
||||||
|
<option value="A"><?= tr('fc_zone_a') ?></option>
|
||||||
|
<option value="B"><?= tr('fc_zone_b') ?></option>
|
||||||
|
<option value="C"><?= tr('fc_zone_c') ?></option>
|
||||||
|
<option value="Autre"><?= tr('fc_zone_other') ?></option>
|
||||||
|
</select>
|
||||||
|
<small class="pf-muted-note"><?= tr('fc_zone_desc') ?></small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pf-form-group" style="margin-top: 1.5rem;">
|
||||||
|
<label class="pf-label"><?= tr('fc_care_modes_label') ?></label>
|
||||||
|
|
||||||
|
<?php if (!empty($kids)): ?>
|
||||||
|
<div id="careModesContainer" style="display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; min-height: 38px; padding: 8px; background: var(--bg-page); border-radius: var(--radius); border: 1px solid var(--border-light);"></div>
|
||||||
|
<div style="display: flex; gap: 8px;">
|
||||||
|
<input type="text" id="inputNewCareMode" class="pf-input" placeholder="<?= tr('fc_add_mode_placeholder') ?>" style="flex: 1;">
|
||||||
|
<button type="button" class="pf-btn btn-secondary" style="padding: 0 16px; border-radius: 6px;" onclick="addCareModeTag()">+</button>
|
||||||
|
</div>
|
||||||
|
<small class="pf-muted-note"><?= tr('fc_care_modes_desc') ?></small>
|
||||||
|
<?php else: ?>
|
||||||
|
<div style="background: var(--bg-subtle); padding: 12px; border-radius: 8px; border: 1px dashed var(--border-main); text-align: center;">
|
||||||
|
<p style="margin: 0 0 10px 0; color: var(--text-muted); font-size: 0.9rem;">
|
||||||
|
<?= tr('fc_empty_kids_msg') ?>
|
||||||
|
</p>
|
||||||
|
<a href="/settings.php" class="pf-btn btn-secondary" style="text-decoration: none; display: inline-block; font-size: 0.85rem;">
|
||||||
|
⚙️ <?= tr('fc_link_settings') ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($kids)): ?>
|
||||||
|
<div style="margin-top: 1rem; display: flex; justify-content: flex-end;">
|
||||||
|
<button type="submit" class="pf-btn pf-btn-primary"><?= tr('save') ?></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="pf-form-group" style="margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--border-light);">
|
||||||
|
<h4 style="margin: 0 0 10px 0; color: var(--text-main);">🏖️ <?= tr('fc_leave_catalog_title') ?></h4>
|
||||||
|
<p style="font-size: 0.85rem; color: var(--text-muted); margin-bottom: 15px;">
|
||||||
|
<?= tr('fc_leave_catalog_desc') ?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div id="leaveTypesContainer" style="display: flex; flex-direction: column; gap: 8px; margin-bottom: 15px;"></div>
|
||||||
|
|
||||||
|
<div style="background: var(--bg-subtle); padding: 12px; border-radius: 8px; border: 1px solid var(--border-light);">
|
||||||
|
<h5 id="leaveTypeFormTitle" style="margin: 0 0 10px 0;">+ <?= tr('fc_add_leave_type') ?></h5>
|
||||||
|
|
||||||
|
<form id="formLeaveType" style="display: flex; flex-direction: column; gap: 10px;">
|
||||||
|
<input type="hidden" id="lt-mode" value="add">
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||||
|
<div style="flex: 0 0 80px;">
|
||||||
|
<label class="pf-label"><?= tr('fc_col_code') ?> *</label>
|
||||||
|
<input type="text" id="lt-code" class="pf-input" maxlength="4" style="text-transform: uppercase;" required>
|
||||||
|
<small class="pf-muted-note" id="lt-code-note"><?= tr('fc_code_irreversible') ?></small>
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; min-width: 150px;">
|
||||||
|
<label class="pf-label"><?= tr('fc_col_label') ?> *</label>
|
||||||
|
<input type="text" id="lt-label" class="pf-input" placeholder="<?= tr('fc_leave_label_ph') ?>" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-top: 5px;">
|
||||||
|
<div style="flex: 1; min-width: 140px;">
|
||||||
|
<label class="pf-label"><?= tr('fc_carry_max') ?></label>
|
||||||
|
<input type="number" step="0.5" id="lt-carry-max" class="pf-input" placeholder="ex: 2" value="0" required>
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; min-width: 140px;">
|
||||||
|
<label class="pf-label"><?= tr('fc_carry_deadline') ?></label>
|
||||||
|
<select id="lt-carry-deadline" class="pf-input">
|
||||||
|
<option value="1"><?= tr('month_01') ?></option>
|
||||||
|
<option value="2"><?= tr('month_02') ?></option>
|
||||||
|
<option value="3"><?= tr('month_03') ?></option>
|
||||||
|
<option value="4"><?= tr('month_04') ?></option>
|
||||||
|
<option value="5"><?= tr('month_05') ?></option>
|
||||||
|
<option value="6"><?= tr('month_06') ?></option>
|
||||||
|
<option value="12"><?= tr('fc_deadline_none') ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 5px;">
|
||||||
|
<button type="button" class="pf-btn btn-secondary" onclick="resetLeaveTypeForm()"><?= tr('btn_cancel') ?></button>
|
||||||
|
<button type="button" class="pf-btn pf-btn-primary" onclick="saveLeaveType()"><?= tr('btn_save') ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="cal-pane-membres" class="cal-settings-pane" style="display: none;">
|
||||||
|
|
||||||
|
<div style="margin-bottom: 1.2rem;">
|
||||||
|
<label class="pf-label"><?= tr('fc_select_member') ?></label>
|
||||||
|
<select id="selectCalMember" class="pf-input" style="width: 100%; font-weight: 600;" onchange="loadMemberConfigView()">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="memberConfigZone" style="background: var(--bg-page); padding: 14px; border-radius: 8px; border: 1px solid var(--border-light); min-height: 150px;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="pf-section">
|
<div class="fc-month-calendar-wrapper">
|
||||||
<div class="fc-month-calendar-wrapper">
|
|
||||||
|
|
||||||
<div class="fc-month-header">
|
<div class="fc-month-header">
|
||||||
|
<div class="fc-month-nav-row">
|
||||||
<div class="fc-month-nav-row">
|
<button id="fc-prev-month" class="fc-nav-button">‹</button>
|
||||||
<button id="fc-prev-month" class="fc-nav-button">‹</button>
|
<div id="fc-smart-date-selector" style="display:flex; align-items:center; justify-content:center; flex-grow:1; gap:6px;">
|
||||||
|
<select id="fc-select-month" class="fc-smart-select"></select>
|
||||||
<div id="fc-smart-date-selector" style="display:flex; align-items:center; justify-content:center; flex-grow:1; gap:6px;">
|
<select id="fc-select-year" class="fc-smart-select"></select>
|
||||||
<select id="fc-select-month" class="fc-smart-select"></select>
|
<span id="fc-multi-month-suffix" style="display:none; font-size:1.3rem; font-weight:800; color:#0f172a; margin-left:4px;"></span>
|
||||||
<select id="fc-select-year" class="fc-smart-select"></select>
|
</div>
|
||||||
<span id="fc-multi-month-suffix" style="display:none; font-size:1.3rem; font-weight:800; color:#0f172a; margin-left:4px;"></span>
|
<button id="fc-next-month" class="fc-nav-button">›</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="fc-next-month" class="fc-nav-button">›</button>
|
<div class="fc-view-controls">
|
||||||
</div>
|
<button id="fc-today-btn" class="fc-today-button" title="<?= tr('fc_today_title') ?>"><?= tr('fc_today_short') ?></button>
|
||||||
|
<div class="fc-view-divider"></div>
|
||||||
<div class="fc-view-controls">
|
<button class="fc-view-button fc-view-button--active" data-view="1month"><?= tr('fc_view_1m') ?></button>
|
||||||
<button id="fc-today-btn" class="fc-today-button" title="<?= tr('fc_today_title') ?>"><?= tr('fc_today_short') ?></button> <div class="fc-view-divider"></div>
|
<button class="fc-view-button" data-view="2months"><?= tr('fc_view_2m') ?></button>
|
||||||
<button class="fc-view-button fc-view-button--active" data-view="1month"><?= tr('fc_view_1m') ?></button>
|
<button class="fc-view-button" data-view="3months"><?= tr('fc_view_3m') ?></button>
|
||||||
<button class="fc-view-button" data-view="2months"><?= tr('fc_view_2m') ?></button>
|
</div>
|
||||||
<button class="fc-view-button" data-view="3months"><?= tr('fc_view_3m') ?></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="fc-calendar-container">
|
<div class="fc-calendar-container">
|
||||||
<div id="fc-month-calendar" class="fc-month-calendar"></div>
|
<div id="fc-month-calendar" class="fc-month-calendar"></div>
|
||||||
<div id="fc-month-selectionMenu" class="fc-selection-menu" style="display:none;"></div>
|
<div id="fc-month-selectionMenu" class="fc-selection-menu" style="display:none;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="fc-month-balances" class="fc-month-balances"></div>
|
<div id="fc-month-balances" class="fc-month-balances"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
<section class="pf-section" style="margin-top: 1.5rem;">
|
||||||
</section>
|
<div class="fc-week-header">
|
||||||
|
<h2><?= tr('fc_weekly_planning') ?></h2>
|
||||||
<section class="pf-section">
|
<div class="fc-week-nav-controls">
|
||||||
<div class="fc-week-header">
|
<button id="fc-prev-school-year" class="fc-nav-button">‹</button>
|
||||||
<h2><?= tr('fc_weekly_planning') ?></h2>
|
<span id="fc-current-school-year-label"></span>
|
||||||
<div class="fc-week-nav-controls">
|
<button id="fc-next-school-year" class="fc-nav-button">›</button>
|
||||||
<button id="fc-prev-school-year" class="fc-nav-button">‹</button>
|
|
||||||
<span id="fc-current-school-year-label"></span>
|
|
||||||
<button id="fc-next-school-year" class="fc-nav-button">›</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="planningTable-wrapper">
|
|
||||||
<table id="planningTable">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th rowspan="3" class="col-month col-sticky-mois"><?= tr('fc_col_month') ?></th>
|
|
||||||
<th rowspan="3" class="col-month col-sticky-mois"><?= tr('fc_col_week_short') ?></th>
|
|
||||||
<th rowspan="3" class="col-day"><?= tr('day_mon') ?></th>
|
|
||||||
<th rowspan="3" class="col-day"><?= tr('day_tue') ?></th>
|
|
||||||
<th rowspan="3" class="col-day"><?= tr('day_wed') ?></th>
|
|
||||||
<th rowspan="3" class="col-day"><?= tr('day_thu') ?></th>
|
|
||||||
<th rowspan="3" class="col-day"><?= tr('day_fri') ?></th>
|
|
||||||
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_off_carole') ?></span></th>
|
|
||||||
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_extra_off') ?></span></th>
|
|
||||||
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_centre') ?></span></th>
|
|
||||||
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_avis') ?></span></th>
|
|
||||||
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_pep_sick') ?></span></th>
|
|
||||||
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_presence') ?></span></th>
|
|
||||||
<th colspan="6" class="col-alex header-group">ALEX</th>
|
|
||||||
<th colspan="6" class="col-laia header-group">LAIA</th>
|
|
||||||
</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>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="planningBody"></tbody>
|
|
||||||
</table>
|
|
||||||
<div id="selectionMenu" class="fc-selection-menu" style="display:none;"></div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="pf-section pf-section--bottom-panels">
|
|
||||||
<div class="fc-bottom-grid">
|
|
||||||
|
|
||||||
<div class="pf-card">
|
|
||||||
<div class="pf-card-title fc-summary-header">
|
|
||||||
<span><?= tr('fc_summary_title') ?></span>
|
|
||||||
<div class="fc-summary-controls">
|
|
||||||
<select id="summType" class="fc-summ-select">
|
|
||||||
<option value="year"><?= tr('fc_summ_year') ?></option>
|
|
||||||
<option value="month"><?= tr('fc_summ_month') ?></option>
|
|
||||||
</select>
|
|
||||||
<select id="summValue" class="fc-summ-select"></select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pf-card-body">
|
|
||||||
<div id="globalSummary" class="fc-summary-vertical"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="pf-card">
|
<div id="planningTable-wrapper">
|
||||||
|
<table id="planningTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th rowspan="3" class="col-month col-sticky-mois rotated-text"><span><?= tr('fc_col_month') ?></span></th>
|
||||||
|
<th rowspan="3" class="col-month col-sticky-sem rotated-text"><span><?= tr('fc_col_week') ?></span></th>
|
||||||
|
<th rowspan="3" class="col-day"><?= tr('day_mon') ?></th>
|
||||||
|
<th rowspan="3" class="col-day"><?= tr('day_tue') ?></th>
|
||||||
|
<th rowspan="3" class="col-day"><?= tr('day_wed') ?></th>
|
||||||
|
<th rowspan="3" class="col-day"><?= tr('day_thu') ?></th>
|
||||||
|
<th rowspan="3" class="col-day"><?= tr('day_fri') ?></th>
|
||||||
|
|
||||||
|
<!-- 1. Congés de l'intervenant -->
|
||||||
|
<?php foreach ($helpers as $helper):
|
||||||
|
$hLeaves = $leaveMatrix[$helper['id']] ?? [];
|
||||||
|
foreach ($hLeaves as $type):
|
||||||
|
?>
|
||||||
|
<th rowspan="3" class="col-total rotated-text"><span><?= htmlspecialchars($type) ?> <?= htmlspecialchars($helper['name']) ?></span></th>
|
||||||
|
<?php endforeach; endforeach; ?>
|
||||||
|
|
||||||
|
<!-- 2. Méthodes de garde (sans Nounou si l'intervenant est configuré) -->
|
||||||
|
<?php foreach ($activeCareModes as $mode):
|
||||||
|
if (strtolower($mode) === 'nounou' && !empty($helpers)) continue;
|
||||||
|
?>
|
||||||
|
<th rowspan="3" class="col-total rotated-text"><span><?= htmlspecialchars($mode) ?></span></th>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<!-- 3. Maladie par enfant -->
|
||||||
|
<?php foreach ($kids as $kid): ?>
|
||||||
|
<th rowspan="3" class="col-total rotated-text"><span><?= tr('fc_menu_sick') ?> <?= htmlspecialchars($kid['name']) ?></span></th>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<?php foreach ($parents as $index => $parent):
|
||||||
|
$parentClass = ($index % 2 === 0) ? 'col-p1' : 'col-p2';
|
||||||
|
$pLeaves = $leaveMatrix[$parent['id']] ?? [];
|
||||||
|
$colspan = count($pLeaves) * 2;
|
||||||
|
?>
|
||||||
|
<?php if ($colspan > 0): ?>
|
||||||
|
<th colspan="<?= $colspan ?>" class="<?= $parentClass ?> header-group"><?= htmlspecialchars(strtoupper($parent['name'])) ?></th>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<?php foreach ($parents as $index => $parent):
|
||||||
|
$parentClass = ($index % 2 === 0) ? 'col-p1' : 'col-p2';
|
||||||
|
$pLeaves = $leaveMatrix[$parent['id']] ?? [];
|
||||||
|
?>
|
||||||
|
<?php foreach ($pLeaves as $type): ?>
|
||||||
|
<th colspan="2" class="<?= $parentClass ?>-sub"><?= htmlspecialchars($type) ?></th>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<?php foreach ($parents as $index => $parent):
|
||||||
|
$parentClass = ($index % 2 === 0) ? 'col-p1' : 'col-p2';
|
||||||
|
$pLeaves = $leaveMatrix[$parent['id']] ?? [];
|
||||||
|
?>
|
||||||
|
<?php foreach ($pLeaves as $type): ?>
|
||||||
|
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-av"><?= tr('fc_col_av') ?></th>
|
||||||
|
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-use"><?= tr('fc_col_use') ?></th>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="planningBody"></tbody>
|
||||||
|
</table>
|
||||||
|
<div id="selectionMenu" class="fc-selection-menu" style="display:none;"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<?php if (!empty($helpers)): ?>
|
||||||
|
<section class="pf-section pf-mt-15">
|
||||||
|
<div id="recapHelperContent"></div>
|
||||||
|
<div style="display: none;">
|
||||||
|
<!-- Champs masqués pour maintenir la compatibilité avec la logique JS actuelle -->
|
||||||
|
<select id="recapHelperMonth" class="pf-input"></select>
|
||||||
|
<select id="recapHelperYear" class="pf-input"></select>
|
||||||
|
<input type="date" id="recapHelperStart" class="pf-input">
|
||||||
|
<input type="date" id="recapHelperEnd" class="pf-input">
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<section class="pf-section pf-section--bottom-panels" style="margin-top: 1.5rem;">
|
||||||
|
<div class="pf-card">
|
||||||
<h2 class="pf-card-title"><?= tr('fc_legend_title') ?></h2>
|
<h2 class="pf-card-title"><?= tr('fc_legend_title') ?></h2>
|
||||||
<div class="pf-card-body">
|
<div class="pf-card-body">
|
||||||
<div class="pf-legend-grid">
|
<div class="pf-legend-grid">
|
||||||
|
<!-- Fériés & Vacances (Fixes) -->
|
||||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-school-holiday"></div><span><?= tr('leg_school_holidays') ?></span></div>
|
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-school-holiday"></div><span><?= tr('leg_school_holidays') ?></span></div>
|
||||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-public-holiday"></div><span><?= tr('leg_public_holiday') ?></span></div>
|
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-public-holiday"></div><span><?= tr('leg_public_holiday') ?></span></div>
|
||||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-off-carole"></div><span><?= tr('leg_off_carole') ?></span></div>
|
|
||||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-extra-off-carole"></div><span><?= tr('leg_extra_off') ?></span></div>
|
<!-- Intervenants (Dynamique selon les congés paramétrés) -->
|
||||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-centre"></div><span><?= tr('leg_centre') ?></span></div>
|
<?php
|
||||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-avis"></div><span><?= tr('leg_avis') ?></span></div>
|
$helperColorIndex = 0;
|
||||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-pep-sick"></div><span><?= tr('leg_pep_sick') ?></span></div>
|
foreach ($helpers as $helper):
|
||||||
|
$hLeaves = $leaveMatrix[$helper['id']] ?? [];
|
||||||
|
foreach ($hLeaves as $type):
|
||||||
|
?>
|
||||||
|
<div class="pf-legend-item">
|
||||||
|
<div class="pf-legend-color fc-bg-palette-<?= $helperColorIndex % 8 ?>"></div>
|
||||||
|
<span><?= htmlspecialchars($type) ?> <?= htmlspecialchars($helper['name']) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php $helperColorIndex++; endforeach; endforeach; ?>
|
||||||
|
|
||||||
|
<!-- Modes de garde (On masque "Nounou" si l'intervenant gère lui-même ses présences) -->
|
||||||
|
<?php foreach ($activeCareModes as $index => $mode):
|
||||||
|
$modeLower = strtolower($mode);
|
||||||
|
if ($modeLower === 'nounou' && !empty($helpers)) continue;
|
||||||
|
if ($modeLower === 'avis'): ?>
|
||||||
|
<div class="pf-legend-item">
|
||||||
|
<img src="/modules/family-calendar/assets/img/avis.svg" style="width:16px;height:16px;object-fit:contain;margin-right:6px;" alt="Avis">
|
||||||
|
<span><?= htmlspecialchars($mode) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php elseif ($modeLower === 'centre'): ?>
|
||||||
|
<div class="pf-legend-item">
|
||||||
|
<span style="font-size:1.1rem; line-height:1; margin-right:6px;">🏫</span>
|
||||||
|
<span><?= htmlspecialchars($mode) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="pf-legend-item">
|
||||||
|
<div class="pf-legend-color" style="background: var(--primary);"></div>
|
||||||
|
<span><?= htmlspecialchars($mode) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif;
|
||||||
|
endforeach; ?>
|
||||||
|
|
||||||
|
<!-- Enfants Malades (Une puce par enfant) -->
|
||||||
|
<?php foreach ($kids as $kid):
|
||||||
|
$color = !empty($kid['color']) ? $kid['color'] : '#e11d48';
|
||||||
|
?>
|
||||||
|
<div class="pf-legend-item">
|
||||||
|
<div class="pf-legend-color" style="background: <?= htmlspecialchars($color) ?>; opacity: 0.8;"></div>
|
||||||
|
<span>Maladie <?= htmlspecialchars($kid['name']) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.I18N = {
|
window.FAMILY_CONFIG = {
|
||||||
...(window.I18N || {}),
|
parents: <?= json_encode($parents) ?>,
|
||||||
'fc_menu_carole': "<?= tr('fc_menu_carole') ?>",
|
kids: <?= json_encode($kids) ?>,
|
||||||
'btn_off': "<?= tr('btn_off') ?>",
|
activeCareModes: <?= json_encode($activeCareModes) ?>,
|
||||||
'btn_extra': "<?= tr('btn_extra') ?>",
|
i18n: {
|
||||||
'leg_centre': "<?= tr('leg_centre') ?>",
|
settings_updated: <?= json_encode(tr('fc_settings_updated')) ?>,
|
||||||
'leg_avis': "<?= tr('leg_avis') ?>",
|
empty_matrix: "Aucun compteur de congés défini pour le moment.",
|
||||||
'leg_pep_sick': "<?= tr('leg_pep_sick') ?>",
|
btn_delete: <?= json_encode(tr('btn_delete') ?? 'Supprimer') ?>
|
||||||
'leg_off_carole': "<?= tr('leg_off_carole') ?>",
|
}
|
||||||
'leg_extra_off': "<?= tr('leg_extra_off') ?>",
|
|
||||||
'leg_presence': "<?= tr('leg_presence') ?>",
|
|
||||||
'fc_menu_kids_leaves': "<?= tr('fc_menu_kids_leaves') ?>",
|
|
||||||
'fc_clear': "<?= tr('fc_clear') ?>",
|
|
||||||
'fc_unit_days': "<?= tr('fc_unit_days') ?>",
|
|
||||||
'vac_toussaint': "<?= tr('vac_toussaint') ?>",
|
|
||||||
'vac_noel': "<?= tr('vac_noel') ?>",
|
|
||||||
'vac_hiver': "<?= tr('vac_hiver') ?>",
|
|
||||||
'vac_printemps': "<?= tr('vac_printemps') ?>",
|
|
||||||
'vac_ascension': "<?= tr('vac_ascension') ?>",
|
|
||||||
'vac_ete': "<?= tr('vac_ete') ?>",
|
|
||||||
'leg_school_holidays': "<?= tr('leg_school_holidays') ?>",
|
|
||||||
'fc_school_year': "<?= tr('fc_school_year') ?>",
|
|
||||||
'fc_modal_holidays_title': "<?= tr('fc_modal_holidays_title') ?>",
|
|
||||||
'fc_alert_burn_days': "<?= tr('fc_alert_burn_days') ?>",
|
|
||||||
'fc_alert_burn_jra': "<?= tr('fc_alert_burn_jra') ?>",
|
|
||||||
'ANNIV': "<?= tr('ANNIV') ?>"
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script src="/modules/family-calendar/family-calendar.js"></script>
|
|
||||||
|
<script src="/modules/family-calendar/family-calendar.js?v=<?= time() ?>"></script>
|
||||||
<?php require __DIR__ . '/footer.php'; ?>
|
<?php require __DIR__ . '/footer.php'; ?>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
// food.php
|
||||||
|
require __DIR__ . '/includes/auth.php';
|
||||||
|
require __DIR__ . '/includes/db.php';
|
||||||
|
require_once __DIR__ . '/includes/i18n.php';
|
||||||
|
require_login('/login.php');
|
||||||
|
|
||||||
|
// Gestion de l'onglet actif
|
||||||
|
$tab = $_GET['tab'] ?? 'meals';
|
||||||
|
|
||||||
|
$pageTitle = tr('mod_food_name');
|
||||||
|
$activePage = "food";
|
||||||
|
$pageCss = "/modules/food/food.css";
|
||||||
|
|
||||||
|
require __DIR__ . '/header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="pf-container">
|
||||||
|
<div class="pf-hero budget-hero">
|
||||||
|
<div class="budget-title-group">
|
||||||
|
<h1>🍳 <?= tr('mod_food_name') ?></h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="budget-tabs-container">
|
||||||
|
<a href="?tab=meals" class="tab-item <?= $tab == 'meals' ? 'active' : '' ?>">
|
||||||
|
<span class="tab-icon">🍽️</span>
|
||||||
|
<span><?= tr('food_tab_meals') ?></span>
|
||||||
|
</a>
|
||||||
|
<a href="?tab=liste" class="tab-item <?= $tab == 'liste' ? 'active' : '' ?>">
|
||||||
|
<span class="tab-icon">🛒</span>
|
||||||
|
<span><?= tr('food_tab_list') ?></span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="pf-section">
|
||||||
|
<?php
|
||||||
|
$allowedTabs = ['meals', 'liste'];
|
||||||
|
if (in_array($tab, $allowedTabs)) {
|
||||||
|
$viewPath = __DIR__ . "/modules/food/views/{$tab}.php";
|
||||||
|
if (file_exists($viewPath)) {
|
||||||
|
require $viewPath;
|
||||||
|
} else {
|
||||||
|
echo "<p>Vue introuvable : " . htmlspecialchars($tab) . "</p>";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "<p>" . tr('error_occured') . "</p>";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php require __DIR__ . '/footer.php'; ?>
|
||||||
+12
-2
@@ -217,15 +217,25 @@ require __DIR__ . '/header.php';
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group"><label class="form-label">Kilométrage</label><input class="form-control" id="vehicle-current-km" name="current_km" type="number" placeholder="85000"></div>
|
<div class="form-group"><label class="form-label">Kilométrage</label><input class="form-control" id="vehicle-current-km" name="current_km" type="number" placeholder="85000"></div>
|
||||||
<div class="form-group"><label class="form-label">Couleur</label><input class="form-control" id="vehicle-color" name="color" placeholder="Blanc nacré"></div>
|
<div class="form-group">
|
||||||
|
<label class="form-label">Conso. moy. (L/100km)</label>
|
||||||
|
<input class="form-control" id="vehicle-consumption" name="consumption" type="number" step="0.1" min="0" placeholder="ex: 6.5">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group"><label class="form-label">Date d'achat</label><input class="form-control" id="vehicle-purchase-date" name="purchase_date" type="date"></div>
|
<div class="form-group"><label class="form-label">Date d'achat</label><input class="form-control" id="vehicle-purchase-date" name="purchase_date" type="date"></div>
|
||||||
<div class="form-group"><label class="form-label">Prix d'achat (€)</label><input class="form-control" id="vehicle-purchase-price" name="purchase_price" type="number" step="0.01"></div>
|
<div class="form-group"><label class="form-label">Prix d'achat (€)</label><input class="form-control" id="vehicle-purchase-price" name="purchase_price" type="number" step="0.01"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group"><label class="form-label">VIN</label><input class="form-control" id="vehicle-vin" name="vin" placeholder="VF1..."></div>
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group"><label class="form-label">VIN</label><input class="form-control" id="vehicle-vin" name="vin" placeholder="VF1..."></div>
|
||||||
|
<div class="form-group"><label class="form-label">Couleur</label><input class="form-control" id="vehicle-color" name="color" placeholder="Blanc nacré"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Photo</label>
|
<label class="form-label">Photo</label>
|
||||||
<input class="form-control" id="vehicle-photo" name="photo" type="file" accept="image/*" onchange="previewPhoto('vehicle-photo','vehicle-photo-preview')">
|
<input class="form-control" id="vehicle-photo" name="photo" type="file" accept="image/*" onchange="previewPhoto('vehicle-photo','vehicle-photo-preview')">
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/meta_db.php';
|
||||||
|
|
||||||
|
$db_host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||||
|
$db_user = getenv('DB_USER') ?: 'househub';
|
||||||
|
$db_pass = getenv('DB_PASS') ?: 'househub_dev';
|
||||||
|
|
||||||
|
echo "<h1>📅 Génération du squelette du calendrier (pf_calendar_weeks)</h1>";
|
||||||
|
|
||||||
|
$monthsFr = [1=>'Janvier', 2=>'Février', 3=>'Mars', 4=>'Avril', 5=>'Mai', 6=>'Juin', 7=>'Juillet', 8=>'Août', 9=>'Septembre', 10=>'Octobre', 11=>'Novembre', 12=>'Décembre'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $meta_pdo->query("SELECT db_name, name FROM families WHERE is_active = 1");
|
||||||
|
$families = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
foreach ($families as $family) {
|
||||||
|
$dbName = $family['db_name'];
|
||||||
|
echo "<h3>Famille : {$family['name']} ($dbName)</h3><ul>";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = new PDO("mysql:host=$db_host;dbname=$dbName;charset=utf8mb4", $db_user, $db_pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||||
|
|
||||||
|
// On utilise REPLACE INTO pour écraser proprement si certaines semaines existent déjà
|
||||||
|
$sql = "REPLACE INTO pf_calendar_weeks
|
||||||
|
(year, week_iso_year, week_iso_number, week_label, month, month_name, week_start_date, mon_date, tue_date, wed_date, thu_date, fri_date)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||||
|
$insertStmt = $pdo->prepare($sql);
|
||||||
|
|
||||||
|
// On génère de l'été 2023 jusqu'à fin 2030 !
|
||||||
|
$startDate = new DateTime('2023-08-28'); // Un lundi
|
||||||
|
$endDate = new DateTime('2030-12-31');
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
while ($startDate <= $endDate) {
|
||||||
|
$mon = clone $startDate;
|
||||||
|
$tue = clone $startDate; $tue->modify('+1 day');
|
||||||
|
$wed = clone $startDate; $wed->modify('+2 days');
|
||||||
|
$thu = clone $startDate; $thu->modify('+3 days');
|
||||||
|
$fri = clone $startDate; $fri->modify('+4 days');
|
||||||
|
|
||||||
|
$year = (int)$mon->format('Y');
|
||||||
|
$month = (int)$mon->format('n');
|
||||||
|
$weekIsoYear = (int)$mon->format('o');
|
||||||
|
$weekIsoNumber = (int)$mon->format('W');
|
||||||
|
$weekLabel = "Semaine " . $weekIsoNumber;
|
||||||
|
$monthName = $monthsFr[$month];
|
||||||
|
|
||||||
|
$insertStmt->execute([
|
||||||
|
$year,
|
||||||
|
$weekIsoYear,
|
||||||
|
$weekIsoNumber,
|
||||||
|
$weekLabel,
|
||||||
|
$month,
|
||||||
|
$monthName,
|
||||||
|
$mon->format('Y-m-d'),
|
||||||
|
$mon->format('Y-m-d'),
|
||||||
|
$tue->format('Y-m-d'),
|
||||||
|
$wed->format('Y-m-d'),
|
||||||
|
$thu->format('Y-m-d'),
|
||||||
|
$fri->format('Y-m-d')
|
||||||
|
]);
|
||||||
|
|
||||||
|
$startDate->modify('+1 week');
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
echo "<li>✅ $count semaines générées (de 2023 à 2030) !</li>";
|
||||||
|
} catch (\PDOException $e) {
|
||||||
|
echo "<li>❌ Erreur : " . $e->getMessage() . "</li>";
|
||||||
|
}
|
||||||
|
echo "</ul>";
|
||||||
|
}
|
||||||
|
echo "<h2>🎉 Terminé ! Tu peux supprimer ce fichier et rafraîchir ton calendrier.</h2>";
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
die("Erreur fatale Meta DB : " . $e->getMessage());
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* HouseHub - CSS Auditor 🦙
|
|
||||||
* Génère un rapport complet de tous les styles (fichiers, balises, inline)
|
|
||||||
*/
|
|
||||||
|
|
||||||
header('Content-Type: text/plain; charset=utf-8');
|
|
||||||
|
|
||||||
$root = __DIR__;
|
|
||||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root));
|
|
||||||
$report = "=== 🦙 RAPPORT CSS COMPLET HOUSEHUB ===\n";
|
|
||||||
$report .= "Généré le : " . date('Y-m-d H:i:s') . "\n\n";
|
|
||||||
|
|
||||||
foreach ($iterator as $file) {
|
|
||||||
if ($file->isDir()) continue;
|
|
||||||
|
|
||||||
$filePath = $file->getPathname();
|
|
||||||
$relativePath = str_replace($root, '', $filePath);
|
|
||||||
$ext = pathinfo($filePath, PATHINFO_EXTENSION);
|
|
||||||
|
|
||||||
// 1. EXTRACTION DES FICHIERS .css
|
|
||||||
if ($ext === 'css') {
|
|
||||||
$report .= "/* 📄 FICHIER SOURCE : $relativePath */\n";
|
|
||||||
$report .= file_get_contents($filePath) . "\n";
|
|
||||||
$report .= str_repeat("-", 40) . "\n\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. EXTRACTION DES BALISES <style> ET ATTRIBUTS style="" DANS LES .php
|
|
||||||
if ($ext === 'php') {
|
|
||||||
$content = file_get_contents($filePath);
|
|
||||||
$foundInFile = false;
|
|
||||||
|
|
||||||
// Extraction des blocs <style>
|
|
||||||
if (preg_match_all('/<style[^>]*>(.*?)<\/style>/is', $content, $matches)) {
|
|
||||||
if (!$foundInFile) { $report .= "/* 📄 FICHIER SOURCE : $relativePath */\n"; $foundInFile = true; }
|
|
||||||
foreach ($matches[1] as $idx => $styleBlock) {
|
|
||||||
$report .= "/* Bloc <style> #$idx */\n";
|
|
||||||
$report .= trim($styleBlock) . "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction des styles inline style="..."
|
|
||||||
// On capture le tag pour donner du contexte au style inline
|
|
||||||
if (preg_match_all('/<([a-z0-9]+)[^>]*?\sstyle=["\']([^"]*?)["\'][^>]*>/i', $content, $matches)) {
|
|
||||||
if (!$foundInFile) { $report .= "/* 📄 FICHIER SOURCE : $relativePath */\n"; $foundInFile = true; }
|
|
||||||
foreach ($matches[0] as $idx => $fullTag) {
|
|
||||||
// On nettoie un peu pour ne garder que l'essentiel du tag et son style
|
|
||||||
$report .= "/* Style Inline #$idx */\n";
|
|
||||||
$report .= trim($fullTag) . "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($foundInFile) {
|
|
||||||
$report .= str_repeat("-", 40) . "\n\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
echo $report;
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<?php
|
|
||||||
// generate_prompt.php - Script pour extraire le code de HouseHub
|
|
||||||
// À lancer via le navigateur (http://localhost:8082/generate_prompt.php) ou en ligne de commande.
|
|
||||||
|
|
||||||
$rootDir = __DIR__;
|
|
||||||
$outputFile = $rootDir . '/househub_source.md';
|
|
||||||
|
|
||||||
// Dossiers à ignorer (pour ne pas polluer le cerveau de l'IA avec des choses inutiles)
|
|
||||||
$ignoredDirs = ['.git', '.devtools', '.gitea', 'assets'];
|
|
||||||
|
|
||||||
// Extensions autorisées (on ne veut que du code, pas d'images)
|
|
||||||
$allowedExtensions = ['php', 'css', 'js', 'html', 'sh'];
|
|
||||||
|
|
||||||
$output = "# 🦙 Source Code HouseHub\n\n";
|
|
||||||
$output .= "> *Généré le " . date('Y-m-d H:i:s') . "*\n\n";
|
|
||||||
|
|
||||||
$iterator = new RecursiveIteratorIterator(
|
|
||||||
new RecursiveDirectoryIterator($rootDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
|
||||||
RecursiveIteratorIterator::SELF_FIRST
|
|
||||||
);
|
|
||||||
|
|
||||||
$fileCount = 0;
|
|
||||||
|
|
||||||
foreach ($iterator as $file) {
|
|
||||||
if ($file->isFile()) {
|
|
||||||
// Chemin relatif propre
|
|
||||||
$relativePath = str_replace($rootDir . DIRECTORY_SEPARATOR, '', $file->getPathname());
|
|
||||||
$relativePath = str_replace('\\', '/', $relativePath); // Uniformiser pour Windows
|
|
||||||
|
|
||||||
// Vérification des dossiers ignorés
|
|
||||||
$skip = false;
|
|
||||||
foreach ($ignoredDirs as $ignored) {
|
|
||||||
if (strpos($relativePath, $ignored) === 0) {
|
|
||||||
$skip = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($skip) continue;
|
|
||||||
|
|
||||||
// Vérification de l'extension
|
|
||||||
$ext = strtolower($file->getExtension());
|
|
||||||
if (!in_array($ext, $allowedExtensions)) continue;
|
|
||||||
|
|
||||||
// On ignore ce script lui-même
|
|
||||||
if ($file->getFilename() === 'generate_prompt.php') continue;
|
|
||||||
|
|
||||||
// Lecture du contenu
|
|
||||||
$content = file_get_contents($file->getPathname());
|
|
||||||
|
|
||||||
// Détermination du langage pour le bloc Markdown
|
|
||||||
$lang = $ext === 'php' ? 'php' : ($ext === 'js' ? 'javascript' : $ext);
|
|
||||||
|
|
||||||
// Formatage pour l'IA
|
|
||||||
$output .= "### 📄 Fichier : `" . $relativePath . "`\n";
|
|
||||||
$output .= "```" . $lang . "\n";
|
|
||||||
$output .= $content . "\n";
|
|
||||||
$output .= "```\n\n";
|
|
||||||
$output .= "---\n\n";
|
|
||||||
|
|
||||||
$fileCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
file_put_contents($outputFile, $output);
|
|
||||||
|
|
||||||
echo "<div style='font-family:sans-serif; padding:20px; background:#f0fdf4; color:#16a34a; border-radius:8px; border:1px solid #bbf7d0;'>";
|
|
||||||
echo "<h2>✅ Succès !</h2>";
|
|
||||||
echo "<p>Le fichier <strong>househub_source.md</strong> a été généré à la racine de ton projet.</p>";
|
|
||||||
echo "<p><strong>$fileCount fichiers</strong> de code ont été fusionnés.</p>";
|
|
||||||
echo "<p><em>Tu peux maintenant donner ce fichier à ta Gem HouseHub !</em></p>";
|
|
||||||
echo "</div>";
|
|
||||||
?>
|
|
||||||
+519
-79
@@ -8,51 +8,36 @@ require_once __DIR__ . '/includes/i18n.php';
|
|||||||
|
|
||||||
if (session_status() === PHP_SESSION_NONE) { session_start(); }
|
if (session_status() === PHP_SESSION_NONE) { session_start(); }
|
||||||
|
|
||||||
// --- 1. CONFIGURATION ---
|
// --- 1. CONFIGURATION DE BASE ---
|
||||||
$year = (int)date('Y');
|
$year = (int)date('Y');
|
||||||
$pageTitle = tr('gift_page_title');
|
$pageTitle = tr('gift_page_title');
|
||||||
$activePage = "gift-list";
|
$activePage = "gift-list";
|
||||||
$mainClass = "pf-gift-list";
|
$mainClass = "pf-gift-list";
|
||||||
$pageCss = "/modules/gift-list/gift-list.css";
|
$pageCss = "/modules/gift-list/gift-list.css";
|
||||||
|
|
||||||
$children = ['Pol', 'Pep', 'Elna', 'Bru', 'Guim'];
|
// --- 2. RÉCUPÉRATION DYNAMIQUE DES DONNÉES ---
|
||||||
$baseAdults = ['Laia', 'Laura', 'Avi Iaia'];
|
// On inclut les enfants du foyer ET les proches enfants
|
||||||
$extraAdults = ['Pauline', 'Papy JC', 'Mamy Caro'];
|
$stmt = $pdo->query("SELECT name FROM pf_people WHERE role IN ('enfant', 'proche_enfant') ORDER BY name ASC");
|
||||||
|
$children = $stmt->fetchAll(PDO::FETCH_COLUMN) ?: ['Aucun enfant configuré'];
|
||||||
|
|
||||||
$adultsByChildForAnniv = [
|
// On inclut les adultes du foyer ET les proches adultes (On exclut nounou et les enfants)
|
||||||
'Pol' => array_merge($baseAdults, $extraAdults),
|
$stmt = $pdo->query("SELECT name FROM pf_people WHERE role NOT IN ('enfant', 'proche_enfant', 'nounou') ORDER BY name ASC");
|
||||||
'Pep' => array_merge($baseAdults, $extraAdults),
|
$allAdultsList = $stmt->fetchAll(PDO::FETCH_COLUMN) ?: ['Aucun adulte configuré'];
|
||||||
'Elna' => $baseAdults,
|
|
||||||
'Bru' => $baseAdults,
|
|
||||||
'Guim' => $baseAdults,
|
|
||||||
];
|
|
||||||
|
|
||||||
$VIEWS = [
|
// On récupère toutes les occasions (avec l'icône)
|
||||||
'nadal' => ['TIO', 'NOEL', 'ROIS'],
|
$stmt = $pdo->query("SELECT code, name, icon, month_date FROM pf_gift_occasions WHERE is_active = 1 ORDER BY sort_order ASC, month_date ASC, id ASC");
|
||||||
'anniversary' => ['ANNIV', 'SANT'],
|
$activeOccasions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
];
|
|
||||||
|
|
||||||
$currentView = strtolower($_GET['view'] ?? ($_SESSION['gift_view'] ?? 'nadal'));
|
if (empty($activeOccasions)) {
|
||||||
if (!isset($VIEWS[$currentView])) $currentView = 'nadal';
|
$activeOccasions = [['code' => 'DEMO', 'name' => 'Fête par défaut', 'icon' => '🎁', 'month_date' => null]];
|
||||||
$_SESSION['gift_view'] = $currentView;
|
}
|
||||||
$allowedOccasions = $VIEWS[$currentView];
|
$allowedOccasionCodes = array_column($activeOccasions, 'code');
|
||||||
|
|
||||||
$allOccasionLabels = [
|
// --- 3. CHARGEMENT DES CADEAUX ---
|
||||||
'TIO' => tr('gift_occ_tio'), 'NOEL' => tr('gift_occ_noel'), 'ROIS' => tr('gift_occ_rois'),
|
$inMarks = implode(',', array_fill(0, count($allowedOccasionCodes), '?'));
|
||||||
'ANNIV' => tr('gift_occ_anniv'), 'SANT' => tr('gift_occ_sant')
|
|
||||||
];
|
|
||||||
$occasionIcons = [
|
|
||||||
'TIO' => '/modules/gift-list/assets/img/tio.png', 'NOEL' => '/modules/gift-list/assets/img/santa.png',
|
|
||||||
'ROIS' => '/modules/gift-list/assets/img/reis.png', 'ANNIV' => '/modules/gift-list/assets/img/corona.png',
|
|
||||||
'SANT' => '/modules/gift-list/assets/img/sant.png'
|
|
||||||
];
|
|
||||||
|
|
||||||
// --- 2. DONNÉES ---
|
|
||||||
$inMarks = implode(',', array_fill(0, count($allowedOccasions), '?'));
|
|
||||||
// Tri: Par Fête, puis Enfant, puis Adulte
|
|
||||||
$sql = "SELECT * FROM pf_gifts WHERE year = ? AND occasion IN ($inMarks) ORDER BY occasion ASC, child_name ASC, adult_name ASC";
|
$sql = "SELECT * FROM pf_gifts WHERE year = ? AND occasion IN ($inMarks) ORDER BY occasion ASC, child_name ASC, adult_name ASC";
|
||||||
$stmt = $pdo->prepare($sql);
|
$stmt = $pdo->prepare($sql);
|
||||||
$stmt->execute(array_merge([$year], $allowedOccasions));
|
$stmt->execute(array_merge([$year], $allowedOccasionCodes));
|
||||||
$gifts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$gifts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$data = [];
|
$data = [];
|
||||||
@@ -62,12 +47,16 @@ foreach ($gifts as $g) {
|
|||||||
$data[$g['occasion']][$g['child_name']]['gifts'][] = $g;
|
$data[$g['occasion']][$g['child_name']]['gifts'][] = $g;
|
||||||
$data[$g['occasion']][$g['child_name']]['totals'][$g['adult_name']] = ($data[$g['occasion']][$g['child_name']]['totals'][$g['adult_name']] ?? 0) + $g['amount'];
|
$data[$g['occasion']][$g['child_name']]['totals'][$g['adult_name']] = ($data[$g['occasion']][$g['child_name']]['totals'][$g['adult_name']] ?? 0) + $g['amount'];
|
||||||
$adultsInView[$g['adult_name']] = true;
|
$adultsInView[$g['adult_name']] = true;
|
||||||
|
if (!empty($g['payer_name'])) {
|
||||||
|
$adultsInView[$g['payer_name']] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$allAdultsList = array_keys($adultsInView); sort($allAdultsList);
|
|
||||||
|
|
||||||
// --- 3. TRICOUNT ---
|
// --- 4. TRICOUNT (Bilan et Remboursements) ---
|
||||||
$people = array_values(array_unique(array_merge($baseAdults, array_column($gifts, 'adult_name'), array_column($gifts, 'payer_name'))));
|
$people = array_values(array_unique(array_merge($allAdultsList, array_keys($adultsInView))));
|
||||||
$people = array_filter($people);
|
$people = array_filter($people);
|
||||||
|
sort($people);
|
||||||
|
|
||||||
$matrix = [];
|
$matrix = [];
|
||||||
foreach ($people as $p1) { foreach ($people as $p2) $matrix[$p1][$p2] = 0.0; }
|
foreach ($people as $p1) { foreach ($people as $p2) $matrix[$p1][$p2] = 0.0; }
|
||||||
|
|
||||||
@@ -94,18 +83,19 @@ for ($i = 0; $i < $countPeople; $i++) {
|
|||||||
require __DIR__ . '/header.php';
|
require __DIR__ . '/header.php';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="pf-container cl-view-<?= htmlspecialchars($currentView) ?>">
|
<div class="pf-container cl-view-dynamic">
|
||||||
|
|
||||||
<div class="cl-titlebar">
|
<div class="cl-titlebar">
|
||||||
<h1>🎁 <?= sprintf(tr('gift_main_title'), $year) ?></h1>
|
<div style="display: flex; align-items: center; gap: 12px;">
|
||||||
<div class="cl-view-switch">
|
<h1 style="margin: 0;">🎁 <?= sprintf(tr('gift_main_title'), $year) ?></h1>
|
||||||
<a href="?view=nadal" class="cl-view-btn <?= $currentView === 'nadal' ? 'is-active' : '' ?>"><?= tr('gift_view_nadal') ?></a>
|
<button id="btn-open-gift-settings" class="btn-settings-gear" title="<?= tr('gift_settings') ?>" style="font-size: 1.4rem; padding: 4px;">
|
||||||
<a href="?view=anniversary" class="cl-view-btn <?= $currentView === 'anniversary' ? 'is-active' : '' ?>"><?= tr('gift_view_anniv') ?></a>
|
⚙️
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pf-filter-bar">
|
<div class="pf-filter-bar">
|
||||||
<span style="font-size:1.2rem;">🔍</span>
|
<span class="pf-filter-icon">🔍</span>
|
||||||
|
|
||||||
<div class="pf-multi-select" id="ms-child">
|
<div class="pf-multi-select" id="ms-child">
|
||||||
<div class="pf-ms-trigger" onclick="toggleMS('ms-child-list', this)">
|
<div class="pf-ms-trigger" onclick="toggleMS('ms-child-list', this)">
|
||||||
@@ -139,16 +129,20 @@ require __DIR__ . '/header.php';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="pf-section">
|
<section class="pf-section">
|
||||||
<?php foreach ($allowedOccasions as $occCode): ?>
|
<?php foreach ($activeOccasions as $occ):
|
||||||
|
$occCode = $occ['code'];
|
||||||
|
// Affichage Brut de la BDD pour le nom de l'occasion
|
||||||
|
$occName = $occ['name'];
|
||||||
|
$occIcon = $occ['icon'] ?: '🎁';
|
||||||
|
?>
|
||||||
<div class="js-occ-section">
|
<div class="js-occ-section">
|
||||||
<h2 class="cl-occasion-title">
|
<h2 class="cl-occasion-title">
|
||||||
<?php if(!empty($occasionIcons[$occCode])): ?><img src="<?= $occasionIcons[$occCode] ?>" class="cl-occasion-icon"><?php endif; ?>
|
<span class="cl-occasion-icon"><?= htmlspecialchars($occIcon) ?></span>
|
||||||
<?= $allOccasionLabels[$occCode] ?>
|
<?= htmlspecialchars($occName) ?>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<?php foreach ($children as $child):
|
<?php foreach ($children as $child):
|
||||||
$childData = $data[$occCode][$child] ?? ['gifts' => [], 'totals' => []];
|
$childData = $data[$occCode][$child] ?? ['gifts' => [], 'totals' => []];
|
||||||
$adultsForThisChild = ($currentView === 'anniversary' && in_array($child, ['Pol', 'Pep'])) ? array_merge($baseAdults, $extraAdults) : $baseAdults;
|
|
||||||
?>
|
?>
|
||||||
<div class="pf-child-section js-child" data-name="<?= htmlspecialchars($child) ?>">
|
<div class="pf-child-section js-child" data-name="<?= htmlspecialchars($child) ?>">
|
||||||
<div class="pf-child-header">
|
<div class="pf-child-header">
|
||||||
@@ -156,7 +150,7 @@ require __DIR__ . '/header.php';
|
|||||||
<button class="pf-btn pf-btn-small btn-add-gift"
|
<button class="pf-btn pf-btn-small btn-add-gift"
|
||||||
data-child="<?= htmlspecialchars($child) ?>"
|
data-child="<?= htmlspecialchars($child) ?>"
|
||||||
data-occ="<?= htmlspecialchars($occCode) ?>"
|
data-occ="<?= htmlspecialchars($occCode) ?>"
|
||||||
data-adults="<?= htmlspecialchars(json_encode(array_values($adultsForThisChild))) ?>">
|
data-adults="<?= htmlspecialchars(json_encode(array_values($allAdultsList))) ?>">
|
||||||
+ <?= tr('gift_add_gift') ?>
|
+ <?= tr('gift_add_gift') ?>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,9 +162,9 @@ require __DIR__ . '/header.php';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if (empty($childData['gifts'])): ?>
|
<?php if (empty($childData['gifts'])): ?>
|
||||||
<p class="js-empty-state" style="color:var(--text-muted); font-size:0.9rem; font-style:italic; margin:0;"><?= tr('gift_empty_state_no_gifts') ?></p>
|
<p class="js-empty-state gift-empty-state"><?= tr('gift_empty_state_no_gifts') ?></p>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<p class="js-empty-state" style="color:var(--text-muted); font-size:0.9rem; font-style:italic; display:none; margin:0;"><?= tr('gift_empty_state_no_filter') ?></p>
|
<p class="js-empty-state gift-empty-state pf-hidden"><?= tr('gift_empty_state_no_filter') ?></p>
|
||||||
<div class="pf-gift-feed">
|
<div class="pf-gift-feed">
|
||||||
<?php foreach ($childData['gifts'] as $g): ?>
|
<?php foreach ($childData['gifts'] as $g): ?>
|
||||||
<div class="pf-gift-card-compact js-gift-card" data-adult="<?= htmlspecialchars($g['adult_name']) ?>">
|
<div class="pf-gift-card-compact js-gift-card" data-adult="<?= htmlspecialchars($g['adult_name']) ?>">
|
||||||
@@ -201,41 +195,42 @@ require __DIR__ . '/header.php';
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="pf-section pf-section--panel" style="border:1px solid var(--border-light); margin-top:40px;">
|
<section class="pf-section pf-section--panel gift-tricount-section">
|
||||||
<h2 style="margin-top:0; color:var(--text-main); font-size:1.3rem;">⚖️ <?= tr('gift_liquidations') ?? 'Bilan & Remboursements' ?></h2>
|
<h2 class="gift-tricount-title">⚖️ <?= tr('gift_liquidations') ?></h2>
|
||||||
|
|
||||||
<?php if (empty($settlements)): ?>
|
<?php if (empty($settlements)): ?>
|
||||||
<p style="color:var(--success); font-weight:700; margin-bottom:0;">✅ <?= tr('gift_no_debt') ?></p>
|
<p class="gift-tricount-success">✅ <?= tr('gift_no_debt') ?></p>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<ul class="pf-tricount-list">
|
<ul class="pf-tricount-list">
|
||||||
<?php foreach ($settlements as $s): ?>
|
<?php foreach ($settlements as $s): ?>
|
||||||
<li class="pf-tricount-item">
|
<li class="pf-tricount-item">
|
||||||
<span><strong><?= htmlspecialchars($s['from']) ?></strong> <?= tr('gift_owes') ?> à <?= htmlspecialchars($s['to']) ?></span>
|
<span><strong><?= htmlspecialchars($s['from']) ?></strong> <?= tr('gift_owes') ?> <?= tr('gift_to') ?> <?= htmlspecialchars($s['to']) ?></span>
|
||||||
<strong style="color:var(--danger);"><?= number_format($s['amount'], 2, ',', ' ') ?> €</strong>
|
<strong class="gift-tricount-debt-amount"><?= number_format($s['amount'], 2, ',', ' ') ?> €</strong>
|
||||||
</li>
|
</li>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</ul>
|
</ul>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<details style="margin-top:20px; padding:12px; border-radius:8px;">
|
<details class="gift-matrix-details">
|
||||||
<summary style="cursor:pointer; font-weight:600; color:var(--text-muted); outline:none;"><?= tr('gift_view_matrix') ?></summary>
|
<summary class="gift-matrix-summary"><?= tr('gift_view_matrix') ?></summary>
|
||||||
<div style="overflow-x:auto; margin-top:15px;">
|
<div class="gift-matrix-wrapper">
|
||||||
<table class="pf-table pf-table--compact cl-debt-matrix">
|
<table class="pf-table pf-table--compact cl-debt-matrix">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="position:sticky; left:0; background:#f8fafc; z-index:2; border-right:2px solid #e2e8f0;"><?= tr('gift_debtor') ?> \ <?= tr('gift_creditor') ?></th>
|
<th class="sticky-col bg-header"><?= tr('gift_debtor') ?> \ <?= tr('gift_creditor') ?></th>
|
||||||
<?php foreach ($people as $p): ?><th><?= htmlspecialchars($p) ?></th><?php endforeach; ?>
|
<?php foreach ($people as $p): ?><th><?= htmlspecialchars($p) ?></th><?php endforeach; ?>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($people as $debtor): ?>
|
<?php foreach ($people as $debtor): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="position:sticky; left:0; background:white; z-index:2; border-right:2px solid #e2e8f0;"><?= htmlspecialchars($debtor) ?></th>
|
<th class="sticky-col bg-body"><?= htmlspecialchars($debtor) ?></th>
|
||||||
<?php foreach ($people as $creditor):
|
<?php foreach ($people as $creditor):
|
||||||
$val = $matrix[$debtor][$creditor] ?? 0;
|
$val = $matrix[$debtor][$creditor] ?? 0;
|
||||||
$display = ($debtor === $creditor) || $val == 0 ? '—' : number_format($val, 2, ',', ' ') . ' €';
|
$display = ($debtor === $creditor) || $val == 0 ? '—' : number_format($val, 2, ',', ' ') . ' €';
|
||||||
|
$cellClass = $val > 0 ? 'gift-cell-danger' : 'gift-cell-muted';
|
||||||
?>
|
?>
|
||||||
<td style="<?= $val > 0 ? 'color:var(--danger); font-weight:700; background:#fef2f2;' : 'color:var(--text-muted);' ?>"><?= $display ?></td>
|
<td class="<?= $cellClass ?>"><?= $display ?></td>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -246,11 +241,12 @@ require __DIR__ . '/header.php';
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 1. Modale CRUD Cadeau -->
|
||||||
<div id="pf-gift-modal" class="pf-modal">
|
<div id="pf-gift-modal" class="pf-modal">
|
||||||
<div class="pf-modal-content" style="max-width: 500px; width: 95%;">
|
<div class="pf-modal-content gift-modal-custom-content">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
<div class="gift-modal-custom-header">
|
||||||
<h3 id="modalTitle" class="pf-modal-title" style="margin:0; border:none; padding:0;">Cadeau</h3>
|
<h3 id="modalTitle" class="pf-modal-title gift-modal-custom-title">Cadeau</h3>
|
||||||
<button type="button" class="btn-modal-close" style="background:none; border:none; font-size:1.8rem; cursor:pointer; color:var(--text-muted); line-height:1;">×</button>
|
<button type="button" class="btn-modal-close gift-modal-close-btn">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" action="/modules/gift-list/save-gift.php" id="giftForm">
|
<form method="post" action="/modules/gift-list/save-gift.php" id="giftForm">
|
||||||
@@ -264,9 +260,9 @@ require __DIR__ . '/header.php';
|
|||||||
<div class="pf-form-group"><label class="pf-label"><?= tr('gift_modal_payer') ?></label><select name="payer_name" id="modalPayer" class="pf-input" required></select></div>
|
<div class="pf-form-group"><label class="pf-label"><?= tr('gift_modal_payer') ?></label><select name="payer_name" id="modalPayer" class="pf-input" required></select></div>
|
||||||
<div class="pf-form-group"><label class="pf-label"><?= tr('gift_modal_gift_name') ?></label><input type="text" name="gift_description" id="modalDesc" class="pf-input" required></div>
|
<div class="pf-form-group"><label class="pf-label"><?= tr('gift_modal_gift_name') ?></label><input type="text" name="gift_description" id="modalDesc" class="pf-input" required></div>
|
||||||
<div class="pf-form-group"><label class="pf-label"><?= tr('gift_modal_price') ?></label><input type="number" step="0.01" name="amount" id="modalAmount" class="pf-input"></div>
|
<div class="pf-form-group"><label class="pf-label"><?= tr('gift_modal_price') ?></label><input type="number" step="0.01" name="amount" id="modalAmount" class="pf-input"></div>
|
||||||
<div class="pf-form-group" style="margin-bottom:25px;"><label class="pf-label"><?= tr('gift_modal_link') ?></label><input type="url" name="product_link" id="modalLink" class="pf-input"></div>
|
<div class="pf-form-group gift-form-group-spaced"><label class="pf-label"><?= tr('gift_modal_link') ?></label><input type="url" name="product_link" id="modalLink" class="pf-input"></div>
|
||||||
|
|
||||||
<div class="modal-footer" style="padding-top:15px; display:flex; justify-content:flex-end; gap:10px;">
|
<div class="modal-footer gift-modal-custom-footer">
|
||||||
<button type="button" class="pf-btn btn-secondary btn-modal-close"><?= tr('btn_cancel') ?></button>
|
<button type="button" class="pf-btn btn-secondary btn-modal-close"><?= tr('btn_cancel') ?></button>
|
||||||
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
|
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
|
||||||
</div>
|
</div>
|
||||||
@@ -274,6 +270,91 @@ require __DIR__ . '/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. Modale Paramètres (Occasions & Proches) -->
|
||||||
|
<div id="modal-gift-settings" class="pf-modal">
|
||||||
|
<div class="pf-modal-content" style="max-width: 600px; width: 90%;">
|
||||||
|
|
||||||
|
<div class="pf-modal-header" style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<h3 class="pf-modal-title" style="margin: 0;">⚙️ <?= tr('gift_settings') ?></h3>
|
||||||
|
<button type="button" class="pf-modal-close" style="background:none; border:none; font-size:1.5rem; cursor:pointer;" onclick="closeGiftSettings()">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 4px; background: var(--bg-page); padding: 4px; border-radius: 8px; margin: 1rem 0;">
|
||||||
|
<button type="button" id="tab-btn-occasions" class="bs-tab-btn active" style="flex: 1; padding: 8px 12px; font-weight: 600; border-radius: 6px; border: none; cursor: pointer; transition: 0.2s;" onclick="switchGLTab('occasions')"><?= tr('gift_tab_occasions') ?></button>
|
||||||
|
<button type="button" id="tab-btn-proches" class="bs-tab-btn" style="flex: 1; padding: 8px 12px; font-weight: 600; border-radius: 6px; border: none; cursor: pointer; transition: 0.2s; background: transparent;" onclick="switchGLTab('proches')"><?= tr('gift_tab_proches') ?></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pf-modal-body" style="min-height: 450px; max-height: 60vh; overflow-y: auto; padding: 0 1rem 1rem 1rem; display: flex; flex-direction: column;">
|
||||||
|
|
||||||
|
<div id="gl-pane-occasions" class="gl-pane active" style="display: block; animation: fadeIn 0.3s ease;">
|
||||||
|
<div id="occasions-list" style="margin-bottom: 20px;">⏳ <?= tr('loading') ?></div>
|
||||||
|
|
||||||
|
<hr style="border:0; border-top: 1px solid var(--border-light); margin: 20px 0;">
|
||||||
|
|
||||||
|
<h5 style="margin: 0 0 15px 0; font-size: 1rem; color: var(--text-main);"><?= tr('gift_settings_add_occ') ?></h5>
|
||||||
|
<form id="form-add-occasion" style="display: flex; flex-direction: column; gap: 16px;">
|
||||||
|
<div>
|
||||||
|
<label class="pf-label" style="display: block; margin-bottom: 6px;"><?= tr('gift_settings_icon') ?></label>
|
||||||
|
<input type="hidden" name="icon" id="add-occ-icon" value="🎁">
|
||||||
|
<div class="emoji-picker" style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||||
|
<div class="emoji-option selected" onclick="selectEmoji(this, 'add-occ-icon', '🎁')">🎁</div>
|
||||||
|
<div class="emoji-option" onclick="selectEmoji(this, 'add-occ-icon', '🎄')">🎄</div>
|
||||||
|
<div class="emoji-option" onclick="selectEmoji(this, 'add-occ-icon', '🎂')">🎂</div>
|
||||||
|
<div class="emoji-option" onclick="selectEmoji(this, 'add-occ-icon', '🎉')">🎉</div>
|
||||||
|
<div class="emoji-option" onclick="selectEmoji(this, 'add-occ-icon', '🎓')">🎓</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
|
<div style="flex: 1; min-width: 150px;">
|
||||||
|
<label class="pf-label" style="display: block; margin-bottom: 6px;"><?= tr('gift_modal_gift_name') ?> *</label>
|
||||||
|
<input type="text" name="name" id="add-occ-name" class="pf-input" style="width: 100%;" required>
|
||||||
|
</div>
|
||||||
|
<div style="width: 110px;">
|
||||||
|
<label class="pf-label" style="display: block; margin-bottom: 6px;"><?= tr('gift_settings_date') ?></label>
|
||||||
|
<input type="text" name="month_date" id="add-occ-date" class="pf-input" style="width: 100%;" placeholder="MM-DD">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 5px;">
|
||||||
|
<button type="button" class="pf-btn btn-secondary" onclick="document.getElementById('form-add-occasion').reset()"><?= tr('btn_cancel') ?></button>
|
||||||
|
<button type="submit" class="pf-btn pf-btn-primary"><?= tr('btn_add') ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="gl-pane-proches" class="gl-pane" style="display: none; animation: fadeIn 0.3s ease;">
|
||||||
|
<div id="proches-list" style="margin-bottom: 20px;">⏳ <?= tr('loading') ?></div>
|
||||||
|
|
||||||
|
<hr style="border:0; border-top: 1px solid var(--border-light); margin: auto 0 20px 0;">
|
||||||
|
|
||||||
|
<h5 style="margin: 0 0 15px 0; font-size: 1rem; color: var(--text-main);"><?= tr('gift_settings_add_proche') ?></h5>
|
||||||
|
<form id="form-add-proche" style="display: flex; flex-direction: column; gap: 16px;">
|
||||||
|
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
|
<div style="flex: 1; min-width: 150px;">
|
||||||
|
<label class="pf-label" style="display: block; margin-bottom: 6px;"><?= tr('gift_ph_proche_name') ?> *</label>
|
||||||
|
<input type="text" name="name" id="add-proche-name" class="pf-input" style="width: 100%;" required>
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; min-width: 150px;">
|
||||||
|
<label class="pf-label" style="display: block; margin-bottom: 6px;"><?= tr('gift_role_label') ?> *</label>
|
||||||
|
<select name="role" id="add-proche-role" class="pf-input" style="width: 100%;" required>
|
||||||
|
<option value="proche_adulte"><?= tr('gift_role_proche_adulte') ?></option>
|
||||||
|
<option value="proche_enfant"><?= tr('gift_role_proche_enfant') ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 5px;">
|
||||||
|
<button type="button" class="pf-btn btn-secondary" onclick="document.getElementById('form-add-proche').reset()"><?= tr('btn_cancel') ?></button>
|
||||||
|
<button type="submit" class="pf-btn pf-btn-primary"><?= tr('btn_add') ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 1. GESTION DU COMPOSANT MULTI-SELECT VANILLA
|
// 1. GESTION DU COMPOSANT MULTI-SELECT VANILLA
|
||||||
@@ -381,14 +462,12 @@ function applyGiftFilters() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 3. LOGIQUE DE LA MODALE & DELEGATION JS
|
// 2. GESTION DE LA MODALE CADEAU (AJOUT/ÉDITION)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
const modal = document.getElementById('pf-gift-modal');
|
const modal = document.getElementById('pf-gift-modal');
|
||||||
const adultSelect = document.getElementById('modalAdult');
|
const adultSelect = document.getElementById('modalAdult');
|
||||||
const payerSelect = document.getElementById('modalPayer');
|
const payerSelect = document.getElementById('modalPayer');
|
||||||
|
|
||||||
// Synchronisation automatique : Adulte -> Payeur
|
|
||||||
if (adultSelect && payerSelect) {
|
if (adultSelect && payerSelect) {
|
||||||
adultSelect.addEventListener('change', function() {
|
adultSelect.addEventListener('change', function() {
|
||||||
let exists = false;
|
let exists = false;
|
||||||
@@ -409,10 +488,7 @@ function populateSelects(adults) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// L'écouteur d'événements global pour tous les boutons !
|
|
||||||
document.body.addEventListener('click', async function(e) {
|
document.body.addEventListener('click', async function(e) {
|
||||||
|
|
||||||
// --- 1. BOUTON FERMER MODALE ---
|
|
||||||
if (e.target.closest('.btn-modal-close')) {
|
if (e.target.closest('.btn-modal-close')) {
|
||||||
if (modal) {
|
if (modal) {
|
||||||
modal.classList.remove('open');
|
modal.classList.remove('open');
|
||||||
@@ -421,7 +497,6 @@ document.body.addEventListener('click', async function(e) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 2. BOUTON AJOUTER (+) ---
|
|
||||||
const btnAdd = e.target.closest('.btn-add-gift');
|
const btnAdd = e.target.closest('.btn-add-gift');
|
||||||
if (btnAdd) {
|
if (btnAdd) {
|
||||||
const childName = btnAdd.dataset.child;
|
const childName = btnAdd.dataset.child;
|
||||||
@@ -453,7 +528,6 @@ document.body.addEventListener('click', async function(e) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 3. BOUTON MODIFIER (CRAYON) ---
|
|
||||||
const btnEdit = e.target.closest('.btn-edit-gift');
|
const btnEdit = e.target.closest('.btn-edit-gift');
|
||||||
if (btnEdit) {
|
if (btnEdit) {
|
||||||
const data = JSON.parse(btnEdit.dataset.gift || '{}');
|
const data = JSON.parse(btnEdit.dataset.gift || '{}');
|
||||||
@@ -485,7 +559,6 @@ document.body.addEventListener('click', async function(e) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 4. BOUTON SUPPRIMER (POUBELLE) ---
|
|
||||||
const btnDel = e.target.closest('.btn-delete-gift');
|
const btnDel = e.target.closest('.btn-delete-gift');
|
||||||
if (btnDel) {
|
if (btnDel) {
|
||||||
const giftId = btnDel.dataset.id;
|
const giftId = btnDel.dataset.id;
|
||||||
@@ -506,10 +579,6 @@ document.body.addEventListener('click', async function(e) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 4. SOUMISSION AJAX DU FORMULAIRE
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
const giftForm = document.getElementById('giftForm');
|
const giftForm = document.getElementById('giftForm');
|
||||||
if (giftForm) {
|
if (giftForm) {
|
||||||
giftForm.addEventListener('submit', async (e) => {
|
giftForm.addEventListener('submit', async (e) => {
|
||||||
@@ -529,6 +598,377 @@ if (giftForm) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 3. GESTION DES PARAMÈTRES (MODALE FÊTES & PROCHES)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
function closeGiftSettings() {
|
||||||
|
document.getElementById('modal-gift-settings').classList.remove('open');
|
||||||
|
document.body.classList.remove('no-scroll');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btn-open-gift-settings')?.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById('modal-gift-settings').classList.add('open');
|
||||||
|
document.body.classList.add('no-scroll');
|
||||||
|
await loadSettingsData();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.btn-close-settings').forEach(btn => btn.addEventListener('click', closeGiftSettings));
|
||||||
|
|
||||||
|
function switchGLTab(tabId) {
|
||||||
|
// Gestion du contenu (Afficher/Cacher)
|
||||||
|
document.getElementById('gl-pane-occasions').style.display = (tabId === 'occasions') ? 'block' : 'none';
|
||||||
|
document.getElementById('gl-pane-proches').style.display = (tabId === 'proches') ? 'block' : 'none';
|
||||||
|
|
||||||
|
// Gestion du style des boutons d'onglet
|
||||||
|
const btnOcc = document.getElementById('tab-btn-occasions');
|
||||||
|
const btnProche = document.getElementById('tab-btn-proches');
|
||||||
|
|
||||||
|
if (tabId === 'occasions') {
|
||||||
|
btnOcc.style.background = 'var(--bg-panel)';
|
||||||
|
btnOcc.style.color = 'var(--primary)';
|
||||||
|
btnOcc.style.boxShadow = 'var(--shadow-sm)';
|
||||||
|
|
||||||
|
btnProche.style.background = 'transparent';
|
||||||
|
btnProche.style.color = 'var(--text-muted)';
|
||||||
|
btnProche.style.boxShadow = 'none';
|
||||||
|
} else {
|
||||||
|
btnProche.style.background = 'var(--bg-panel)';
|
||||||
|
btnProche.style.color = 'var(--primary)';
|
||||||
|
btnProche.style.boxShadow = 'var(--shadow-sm)';
|
||||||
|
|
||||||
|
btnOcc.style.background = 'transparent';
|
||||||
|
btnOcc.style.color = 'var(--text-muted)';
|
||||||
|
btnOcc.style.boxShadow = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasSettingsChanged = false;
|
||||||
|
|
||||||
|
async function loadSettingsData() {
|
||||||
|
try {
|
||||||
|
const res = await pachaFetch('/modules/gift-list/api-settings.php?action=get_all');
|
||||||
|
if (!res.success) throw new Error(res.error);
|
||||||
|
|
||||||
|
renderOccasions(res.data.occasions);
|
||||||
|
renderProches(res.data.proches);
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('occasions-list').innerHTML = `<div class="pf-alert pf-alert--error">Erreur : ${e.message}</div>`;
|
||||||
|
document.getElementById('proches-list').innerHTML = `<div class="pf-alert pf-alert--error">Erreur : ${e.message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOccasions(occasions) {
|
||||||
|
const container = document.getElementById('occasions-list');
|
||||||
|
if (!occasions.length) { container.innerHTML = `<em class="gift-empty-state">Vide</em>`; return; }
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
occasions.forEach(occ => {
|
||||||
|
const isChecked = occ.is_active == 1 ? 'checked' : '';
|
||||||
|
// ⚠️ Remarque l'ajout de id="row-occ-${occ.id}" ici :
|
||||||
|
html += `
|
||||||
|
<div class="gl-list-item" id="row-occ-${occ.id}">
|
||||||
|
<div>
|
||||||
|
<span style="font-size:1.2rem; margin-right:8px;">${occ.icon || '🎁'}</span>
|
||||||
|
<strong>${occ.name}</strong>
|
||||||
|
<small style="color:var(--text-muted); margin-left:8px;">${occ.month_date || ''}</small>
|
||||||
|
</div>
|
||||||
|
<div class="pf-flex-gap-8 align-center">
|
||||||
|
<label style="display:flex; align-items:center; cursor:pointer;">
|
||||||
|
<input type="checkbox" class="pf-checkbox-lg" ${isChecked} onchange="toggleOccasion(${occ.id}, this.checked)">
|
||||||
|
</label>
|
||||||
|
<button type="button" class="btn-icon-action edit" onclick="transformRowToEditOccasion('row-occ-${occ.id}', ${occ.id}, '${occ.name.replace(/'/g, "\\'")}', '${occ.icon}', '${occ.month_date || ''}')" title="${tr('btn_edit')}">✏️</button>
|
||||||
|
<button type="button" class="btn-icon-action delete" onclick="deleteOccasion(${occ.id})" title="${tr('btn_delete')}">🗑️</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProches(proches) {
|
||||||
|
const container = document.getElementById('proches-list');
|
||||||
|
if (!proches.length) { container.innerHTML = `<em class="gift-empty-state">Aucun proche configuré.</em>`; return; }
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
proches.forEach(p => {
|
||||||
|
const roleLabel = p.role === 'proche_adulte' ? (tr('gift_role_proche_adulte') || 'Adulte') : (tr('gift_role_proche_enfant') || 'Enfant');
|
||||||
|
html += `
|
||||||
|
<div class="gl-list-item" id="row-proche-${p.id}">
|
||||||
|
<div><strong>${p.name}</strong> <small style="color:var(--text-muted);">— ${roleLabel}</small></div>
|
||||||
|
<div class="pf-flex-gap-8 align-center">
|
||||||
|
<button type="button" class="btn-icon-action edit" onclick="transformRowToEditProche('row-proche-${p.id}', ${p.id}, '${p.name.replace(/'/g, "\\'")}', '${p.role}')" title="${tr('btn_edit')}">✏️</button>
|
||||||
|
<button type="button" class="btn-icon-action delete" onclick="deleteProche(${p.id})" title="${tr('btn_delete')}">🗑️</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// CRUD : OCCASIONS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
// 1. Ajouter une occasion
|
||||||
|
document.getElementById('form-add-occasion')?.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = e.target.querySelector('button[type="submit"]');
|
||||||
|
btn.disabled = true;
|
||||||
|
hasSettingsChanged = true;
|
||||||
|
|
||||||
|
const fd = new FormData(e.target);
|
||||||
|
fd.append('action', 'save_occasion');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
|
||||||
|
|
||||||
|
// On vide le formulaire
|
||||||
|
e.target.reset();
|
||||||
|
|
||||||
|
// On remet l'émoji par défaut visuellement
|
||||||
|
document.querySelectorAll('#form-add-occasion .emoji-option').forEach(el => el.classList.remove('selected'));
|
||||||
|
document.querySelector('#form-add-occasion .emoji-option').classList.add('selected');
|
||||||
|
document.getElementById('add-occ-icon').value = '🎁';
|
||||||
|
|
||||||
|
showToast("Fête ajoutée avec succès !", "success");
|
||||||
|
loadSettingsData(); // Recharge la liste
|
||||||
|
} catch (err) {
|
||||||
|
showToast("Erreur lors de l'ajout", "error");
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Sauvegarder la modification (In-place)
|
||||||
|
async function saveInlineOccasion(id) {
|
||||||
|
hasSettingsChanged = true;
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', 'save_occasion');
|
||||||
|
fd.append('id', id);
|
||||||
|
fd.append('icon', document.getElementById(`inline-occ-icon-${id}`).value);
|
||||||
|
fd.append('name', document.getElementById(`inline-occ-name-${id}`).value);
|
||||||
|
fd.append('month_date', document.getElementById(`inline-occ-date-${id}`).value);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
|
||||||
|
showToast("Fête modifiée avec succès !", "success");
|
||||||
|
loadSettingsData();
|
||||||
|
} catch (err) {
|
||||||
|
showToast("Erreur lors de la modification", "error");
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Supprimer une occasion
|
||||||
|
async function deleteOccasion(id) {
|
||||||
|
if (!confirm(tr('confirm_delete') || 'Supprimer définitivement ?')) return;
|
||||||
|
hasSettingsChanged = true;
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', 'delete_occasion');
|
||||||
|
fd.append('id', id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
|
||||||
|
showToast("Fête supprimée.", "success");
|
||||||
|
loadSettingsData();
|
||||||
|
} catch (err) {
|
||||||
|
showToast("Erreur lors de la suppression", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Activer / Désactiver (Checkbox)
|
||||||
|
async function toggleOccasion(id, state) {
|
||||||
|
hasSettingsChanged = true;
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', 'toggle_occasion');
|
||||||
|
fd.append('id', id);
|
||||||
|
fd.append('state', state ? 1 : 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
|
||||||
|
showToast(state ? "Fête activée" : "Fête masquée", "success");
|
||||||
|
} catch (err) {
|
||||||
|
showToast("Erreur de mise à jour", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// CRUD : PROCHES
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
// 1. Ajouter un proche
|
||||||
|
document.getElementById('form-add-proche')?.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = e.target.querySelector('button[type="submit"]');
|
||||||
|
btn.disabled = true;
|
||||||
|
hasSettingsChanged = true;
|
||||||
|
|
||||||
|
const fd = new FormData(e.target);
|
||||||
|
fd.append('action', 'save_proche');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
|
||||||
|
e.target.reset();
|
||||||
|
showToast("Proche ajouté avec succès !", "success");
|
||||||
|
loadSettingsData();
|
||||||
|
} catch (err) {
|
||||||
|
showToast("Erreur lors de l'ajout", "error");
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Sauvegarder la modification (In-place)
|
||||||
|
async function saveInlineProche(id) {
|
||||||
|
hasSettingsChanged = true;
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', 'save_proche');
|
||||||
|
fd.append('id', id);
|
||||||
|
fd.append('name', document.getElementById(`inline-proche-name-${id}`).value);
|
||||||
|
fd.append('role', document.getElementById(`inline-proche-role-${id}`).value);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
|
||||||
|
showToast("Proche mis à jour !", "success");
|
||||||
|
loadSettingsData();
|
||||||
|
} catch (err) {
|
||||||
|
showToast("Erreur lors de la mise à jour", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Supprimer un proche
|
||||||
|
async function deleteProche(id) {
|
||||||
|
if (!confirm(tr('confirm_delete') || 'Supprimer ce proche ?')) return;
|
||||||
|
hasSettingsChanged = true;
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', 'delete_proche');
|
||||||
|
fd.append('id', id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
|
||||||
|
showToast("Proche retiré de la liste.", "success");
|
||||||
|
loadSettingsData();
|
||||||
|
} catch (err) {
|
||||||
|
showToast("Erreur lors de la suppression", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rechargement intelligent lors de la fermeture si modifications
|
||||||
|
document.getElementById('modal-gift-settings').addEventListener('click', (e) => {
|
||||||
|
if (e.target.id === 'modal-gift-settings' || e.target.classList.contains('btn-close-settings')) {
|
||||||
|
if (hasSettingsChanged) {
|
||||||
|
showToast(tr('gift_settings_saved') || 'Paramètres mis à jour !', 'success');
|
||||||
|
setTimeout(() => window.location.reload(), 1000);
|
||||||
|
} else {
|
||||||
|
closeGiftSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fonction pour gérer la sélection dans la grille d'émojis (formulaire d'ajout)
|
||||||
|
function selectEmoji(element, targetInputId, emoji) {
|
||||||
|
document.getElementById(targetInputId).value = emoji;
|
||||||
|
// Mise à jour visuelle de la sélection
|
||||||
|
const siblings = element.parentNode.querySelectorAll('.emoji-option');
|
||||||
|
siblings.forEach(el => el.classList.remove('selected'));
|
||||||
|
element.classList.add('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// LOGIQUE D'ÉDITION IN-PLACE (OCCASIONS)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
// On crée une mémoire pour stocker le HTML d'origine de chaque ligne
|
||||||
|
const rowStates = {};
|
||||||
|
|
||||||
|
function transformRowToEditOccasion(rowId, id, name, icon, date) {
|
||||||
|
const row = document.getElementById(rowId);
|
||||||
|
|
||||||
|
// On sauvegarde l'état proprement en JS (fini les bugs d'affichage !)
|
||||||
|
rowStates[rowId] = row.innerHTML;
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<div style="display: flex; flex-wrap: wrap; gap: 8px; align-items: center; width: 100%; background: var(--bg-subtle); padding: 8px; border-radius: 8px; border: 1px solid var(--border-light);">
|
||||||
|
<select id="inline-occ-icon-${id}" class="pf-input" style="width: 60px; padding: 6px; margin: 0;">
|
||||||
|
<option value="🎁" ${icon==='🎁'?'selected':''}>🎁</option>
|
||||||
|
<option value="🎄" ${icon==='🎄'?'selected':''}>🎄</option>
|
||||||
|
<option value="🎂" ${icon==='🎂'?'selected':''}>🎂</option>
|
||||||
|
<option value="🎉" ${icon==='🎉'?'selected':''}>🎉</option>
|
||||||
|
<option value="🎓" ${icon==='🎓'?'selected':''}>🎓</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" id="inline-occ-name-${id}" value="${name}" class="pf-input" style="flex: 1; min-width: 100px; padding: 6px; margin: 0;">
|
||||||
|
<input type="text" id="inline-occ-date-${id}" value="${date}" class="pf-input" style="width: 70px; padding: 6px; margin: 0;" placeholder="MM-JJ">
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 4px; margin-left: auto;">
|
||||||
|
<button type="button" onclick="saveInlineOccasion(${id})" class="pf-btn pf-btn-primary" style="padding: 6px 10px;">💾</button>
|
||||||
|
<button type="button" onclick="cancelInlineEdit('${rowId}')" class="pf-btn btn-secondary" style="padding: 6px 10px;">❌</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveInlineOccasion(id) {
|
||||||
|
hasSettingsChanged = true;
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', 'save_occasion');
|
||||||
|
fd.append('id', id);
|
||||||
|
fd.append('icon', document.getElementById(`inline-occ-icon-${id}`).value);
|
||||||
|
fd.append('name', document.getElementById(`inline-occ-name-${id}`).value);
|
||||||
|
fd.append('month_date', document.getElementById(`inline-occ-date-${id}`).value);
|
||||||
|
|
||||||
|
// On met à jour en BDD puis on recharge la liste !
|
||||||
|
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
|
||||||
|
loadSettingsData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// LOGIQUE D'ÉDITION IN-PLACE (PROCHES)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
function transformRowToEditProche(rowId, id, name, role) {
|
||||||
|
const row = document.getElementById(rowId);
|
||||||
|
rowStates[rowId] = row.innerHTML;
|
||||||
|
|
||||||
|
const roleAdulte = window.I18N ? window.I18N['gift_role_proche_adulte'] : 'Adulte';
|
||||||
|
const roleEnfant = window.I18N ? window.I18N['gift_role_proche_enfant'] : 'Enfant';
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<div style="display: flex; flex-wrap: wrap; gap: 8px; align-items: center; width: 100%; background: var(--bg-subtle); padding: 8px; border-radius: 8px; border: 1px solid var(--border-light);">
|
||||||
|
<input type="text" id="inline-proche-name-${id}" value="${name}" class="pf-input" style="flex: 1; min-width: 100px; padding: 6px; margin: 0;">
|
||||||
|
<select id="inline-proche-role-${id}" class="pf-input" style="width: 130px; padding: 6px; margin: 0;">
|
||||||
|
<option value="proche_adulte" ${role==='proche_adulte'?'selected':''}>${roleAdulte}</option>
|
||||||
|
<option value="proche_enfant" ${role==='proche_enfant'?'selected':''}>${roleEnfant}</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 4px; margin-left: auto;">
|
||||||
|
<button type="button" onclick="saveInlineProche(${id})" class="pf-btn pf-btn-primary" style="padding: 6px 10px;">💾</button>
|
||||||
|
<button type="button" onclick="cancelInlineEdit('${rowId}')" class="pf-btn btn-secondary" style="padding: 6px 10px;">❌</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveInlineProche(id) {
|
||||||
|
hasSettingsChanged = true;
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', 'save_proche');
|
||||||
|
fd.append('id', id);
|
||||||
|
fd.append('name', document.getElementById(`inline-proche-name-${id}`).value);
|
||||||
|
fd.append('role', document.getElementById(`inline-proche-role-${id}`).value);
|
||||||
|
|
||||||
|
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
|
||||||
|
loadSettingsData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour restaurer la ligne (sans casser le HTML)
|
||||||
|
function cancelInlineEdit(rowId) {
|
||||||
|
document.getElementById(rowId).innerHTML = rowStates[rowId];
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<?php require __DIR__ . '/footer.php'; ?>
|
<?php require __DIR__ . '/footer.php'; ?>
|
||||||
+896
-68
File diff suppressed because it is too large.
Load diff
+7
-5
@@ -55,7 +55,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
|||||||
<?php if (in_array('garage', $mods)): ?><a href="/garage.php" class="pf-nav-link <?= $activePage === 'garage' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_garage') ?></a><?php endif; ?>
|
<?php if (in_array('garage', $mods)): ?><a href="/garage.php" class="pf-nav-link <?= $activePage === 'garage' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_garage') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('memo', $mods)): ?><a href="/memo.php" class="pf-nav-link <?= $activePage === 'memo' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_memo') ?></a><?php endif; ?>
|
<?php if (in_array('memo', $mods)): ?><a href="/memo.php" class="pf-nav-link <?= $activePage === 'memo' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_memo') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('todo', $mods)): ?><a href="/todo.php" class="pf-nav-link <?= $activePage === 'todo' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_todo') ?></a><?php endif; ?>
|
<?php if (in_array('todo', $mods)): ?><a href="/todo.php" class="pf-nav-link <?= $activePage === 'todo' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_todo') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('liste', $mods)): ?><a href="/liste.php" class="pf-nav-link <?= $activePage === 'liste' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_liste') ?></a><?php endif; ?>
|
<?php if (in_array('food', $mods)): ?><a href="/food.php" class="pf-nav-link <?= $activePage === 'food' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_food') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('calendar_ios', $mods)): ?><a href="/calendar-ios.php" class="pf-nav-link <?= $activePage === 'calendar-ios' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_calendar_ios') ?></a><?php endif; ?>
|
<?php if (in_array('calendar_ios', $mods)): ?><a href="/calendar-ios.php" class="pf-nav-link <?= $activePage === 'calendar-ios' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_calendar_ios') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('printvault', $mods)): ?><a href="/printvault.php" class="pf-nav-link <?= $activePage === 'printvault' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_printvault') ?></a><?php endif; ?>
|
<?php if (in_array('printvault', $mods)): ?><a href="/printvault.php" class="pf-nav-link <?= $activePage === 'printvault' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_printvault') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('planka', $mods)): ?><a href="/planka.php" class="pf-nav-link <?= $activePage === 'planka' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_planka') ?></a><?php endif; ?>
|
<?php if (in_array('planka', $mods)): ?><a href="/planka.php" class="pf-nav-link <?= $activePage === 'planka' ? 'pf-nav-link--active' : '' ?>"><?= tr('menu_planka') ?></a><?php endif; ?>
|
||||||
@@ -106,7 +106,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr';
|
|||||||
<?php if (in_array('garage', $mods)): ?><a href="/garage.php" class="pf-mobile-nav-link">🚗 <?= tr('menu_garage') ?></a><?php endif; ?>
|
<?php if (in_array('garage', $mods)): ?><a href="/garage.php" class="pf-mobile-nav-link">🚗 <?= tr('menu_garage') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('memo', $mods)): ?><a href="/memo.php" class="pf-mobile-nav-link">📝 <?= tr('menu_memo') ?></a><?php endif; ?>
|
<?php if (in_array('memo', $mods)): ?><a href="/memo.php" class="pf-mobile-nav-link">📝 <?= tr('menu_memo') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('todo', $mods)): ?><a href="/todo.php" class="pf-mobile-nav-link">✅ <?= tr('menu_todo') ?></a><?php endif; ?>
|
<?php if (in_array('todo', $mods)): ?><a href="/todo.php" class="pf-mobile-nav-link">✅ <?= tr('menu_todo') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('liste',$mods)): ?><a href="/liste.php" class="pf-mobile-nav-link">🛒 <?= tr('menu_liste') ?></a><?php endif; ?>
|
<?php if (in_array('food', $mods)): ?><a href="/food.php" class="pf-mobile-nav-link">🍳 <?= tr('menu_food') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('calendar_ios', $mods)): ?><a href="/calendar-ios.php" class="pf-mobile-nav-link">📱 <?= tr('menu_calendar_ios') ?></a><?php endif; ?>
|
<?php if (in_array('calendar_ios', $mods)): ?><a href="/calendar-ios.php" class="pf-mobile-nav-link">📱 <?= tr('menu_calendar_ios') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('printvault', $mods)): ?><a href="/printvault.php" class="pf-mobile-nav-link">🖨️ <?= tr('menu_printvault') ?></a><?php endif; ?>
|
<?php if (in_array('printvault', $mods)): ?><a href="/printvault.php" class="pf-mobile-nav-link">🖨️ <?= tr('menu_printvault') ?></a><?php endif; ?>
|
||||||
<?php if (in_array('planka', $mods)): ?><a href="/planka.php" class="pf-mobile-nav-link">📋 <?= tr('menu_planka') ?></a><?php endif; ?>
|
<?php if (in_array('planka', $mods)): ?><a href="/planka.php" class="pf-mobile-nav-link">📋 <?= tr('menu_planka') ?></a><?php endif; ?>
|
||||||
@@ -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); ?>;
|
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 = {
|
window.CONFIG = {
|
||||||
ID_ALEX: <?php echo defined('ID_ALEX') ? ID_ALEX : 2; ?>,
|
// Liste complète des profils de la famille (Membres, Nounou, Enfants)
|
||||||
ID_LAIA: <?php echo defined('ID_LAIA') ? ID_LAIA : 3; ?>,
|
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 : "€"; ?>',
|
CURRENCY: '<?php echo defined('CURRENCY') ? CURRENCY : "€"; ?>',
|
||||||
ZONE_SCOLAIRE: '<?php echo defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : "C"; ?>'
|
ZONE_SCOLAIRE: '<?php echo defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : "C"; ?>'
|
||||||
};
|
};
|
||||||
|
|||||||
+16
-2
@@ -22,6 +22,9 @@ $pageCss = "/modules/holidays/holidays.css";
|
|||||||
|
|
||||||
require __DIR__ . '/header.php';
|
require __DIR__ . '/header.php';
|
||||||
|
|
||||||
|
$stmtVehicles = $pdo->query("SELECT id, name FROM pf_vehicles ORDER BY name ASC");
|
||||||
|
$garageVehicles = $stmtVehicles->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
// 3. ROUTEUR DU MODULE VACANCES
|
// 3. ROUTEUR DU MODULE VACANCES
|
||||||
if ($tab === 'holiday_detail' && isset($_GET['id'])) {
|
if ($tab === 'holiday_detail' && isset($_GET['id'])) {
|
||||||
// Si on demande le détail ET qu'un ID est fourni
|
// Si on demande le détail ET qu'un ID est fourni
|
||||||
@@ -31,7 +34,18 @@ if ($tab === 'holiday_detail' && isset($_GET['id'])) {
|
|||||||
require __DIR__ . '/modules/holidays/views/list.php';
|
require __DIR__ . '/modules/holidays/views/list.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Inclusion du JS global du module (Pont i18n déjà géré dans le header)
|
// 4. Inclusion des librairies globales du module (Flatpickr)
|
||||||
echo '<script src="/modules/holidays/holidays.js"></script>';
|
?>
|
||||||
|
<!-- Flatpickr (Vanilla JS Date Range) -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/themes/dark.css" id="flatpickr-dark-theme" disabled>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/fr.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/cat.js"></script>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$jsPath = __DIR__ . '/modules/holidays/holidays.js';
|
||||||
|
$jsVersion = file_exists($jsPath) ? filemtime($jsPath) : time();
|
||||||
|
echo '<script src="/modules/holidays/holidays.js?v=' . $jsVersion . '"></script>';
|
||||||
|
|
||||||
require __DIR__ . '/footer.php';
|
require __DIR__ . '/footer.php';
|
||||||
@@ -4,6 +4,12 @@ if (session_status() === PHP_SESSION_NONE) {
|
|||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie qu'un utilisateur est connecté.
|
||||||
|
* Si non, le redirige vers la page de login avec ?redirect=URL_DEMANDEE
|
||||||
|
*
|
||||||
|
* @param string|null $loginPage URL de la page de login (par défaut /login.php)
|
||||||
|
*/
|
||||||
/**
|
/**
|
||||||
* Vérifie qu'un utilisateur est connecté.
|
* Vérifie qu'un utilisateur est connecté.
|
||||||
* Si non, le redirige vers la page de login avec ?redirect=URL_DEMANDEE
|
* Si non, le redirige vers la page de login avec ?redirect=URL_DEMANDEE
|
||||||
@@ -16,6 +22,47 @@ function require_login(?string $loginPage = '/login.php'): void
|
|||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- NOUVEAU : Auto-login via Cookie ---
|
||||||
|
if (empty($_SESSION['user']) && isset($_COOKIE['hh_remember'])) {
|
||||||
|
$cookieParts = explode(':', $_COOKIE['hh_remember']);
|
||||||
|
|
||||||
|
if (count($cookieParts) === 2) {
|
||||||
|
$cookieUserId = (int)$cookieParts[0];
|
||||||
|
$cookieToken = $cookieParts[1];
|
||||||
|
|
||||||
|
global $meta_pdo;
|
||||||
|
require_once __DIR__ . '/meta_db.php';
|
||||||
|
|
||||||
|
$stmt = $meta_pdo->prepare("
|
||||||
|
SELECT u.id, u.username, u.password_hash, u.display_name, u.family_id, u.is_admin, u.is_active, u.lang, u.remember_token,
|
||||||
|
f.db_name, f.is_active as family_active, f.enabled_modules
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN families f ON f.id = u.family_id
|
||||||
|
WHERE u.id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$cookieUserId]);
|
||||||
|
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// On vérifie que le token correspond au hash stocké
|
||||||
|
if ($user && $user['remember_token'] && hash_equals($user['remember_token'], hash('sha256', $cookieToken))) {
|
||||||
|
if ($user['is_active'] && (!$user['family_id'] || $user['family_active'])) {
|
||||||
|
// Connexion silencieuse réussie, on peuple la session
|
||||||
|
$_SESSION['user'] = [
|
||||||
|
'id' => (int)$user['id'],
|
||||||
|
'username' => $user['username'],
|
||||||
|
'display_name' => $user['display_name'],
|
||||||
|
'family_id' => (int)$user['family_id'],
|
||||||
|
'is_admin' => (bool)$user['is_admin'],
|
||||||
|
];
|
||||||
|
$_SESSION['family_db'] = $user['db_name'];
|
||||||
|
$_SESSION['app_lang'] = $user['lang'] ?? 'fr';
|
||||||
|
$_SESSION['enabled_modules'] = json_decode($user['enabled_modules'] ?? '["calendar","budget","holidays","gifts","calendar_ios"]', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ---------------------------------------
|
||||||
|
|
||||||
if (empty($_SESSION['user'])) {
|
if (empty($_SESSION['user'])) {
|
||||||
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) || (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false)) {
|
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) || (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false)) {
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
@@ -54,3 +101,8 @@ function verify_csrf(?string $token): bool
|
|||||||
}
|
}
|
||||||
return hash_equals($_SESSION['csrf_token'], $token);
|
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);
|
|
||||||
}
|
|
||||||
+41
-38
@@ -1,56 +1,59 @@
|
|||||||
<?php
|
<?php
|
||||||
|
// includes/db.php
|
||||||
|
|
||||||
if (file_exists(__DIR__ . '/config.php')) {
|
if (file_exists(__DIR__ . '/config.php')) {
|
||||||
require_once __DIR__ . '/config.php';
|
require_once __DIR__ . '/config.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
$host = getenv('DB_HOST') ?: 'househub-db';
|
$envHost = getenv('DB_HOST');
|
||||||
$user = getenv('DB_USER') ?: 'househub';
|
|
||||||
$pass = getenv('DB_PASS') ?: 'changeme';
|
|
||||||
$db = $_SESSION['family_db'] ?? null;
|
|
||||||
|
|
||||||
if (!$db) {
|
// 1. Détection stricte de l'environnement
|
||||||
$current = basename($_SERVER['PHP_SELF'] ?? '');
|
if ($envHost === 'househub-db') {
|
||||||
if (!in_array($current, ['login.php', 'register.php'])) {
|
// Docker
|
||||||
header('Location: /login.php');
|
$host = 'househub-db';
|
||||||
exit;
|
$user = getenv('DB_USER') ?: 'househub';
|
||||||
}
|
$pass = getenv('DB_PASS') ?: 'changeme';
|
||||||
return;
|
} else {
|
||||||
|
// XAMPP
|
||||||
|
$host = '127.0.0.1'; // Plus stable que 'localhost' sous XAMPP
|
||||||
|
$user = 'root';
|
||||||
|
$pass = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Multi-tenant strict : cibler la DB de la famille courante
|
||||||
|
$db = 'househub_meta'; // Fallback par défaut
|
||||||
|
if (!empty($_SESSION['user']['family_id'])) {
|
||||||
|
$db = 'househub_f' . (int)$_SESSION['user']['family_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$charset = 'utf8mb4';
|
||||||
|
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_general_ci",
|
||||||
|
PDO::ATTR_TIMEOUT => 30,
|
||||||
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$pdo = new PDO(
|
$pdo = new PDO($dsn, $user, $pass, $options);
|
||||||
"mysql:host=$host;dbname=$db;charset=utf8mb4",
|
|
||||||
$user, $pass,
|
|
||||||
[
|
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
||||||
PDO::ATTR_EMULATE_PREPARES => false,
|
|
||||||
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_general_ci",
|
|
||||||
PDO::ATTR_TIMEOUT => 30,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
$pdo->exec("SET collation_connection = utf8mb4_general_ci");
|
|
||||||
|
|
||||||
try {
|
// Forcer la collation
|
||||||
$foyer = $pdo->query("SELECT currency, zone_scolaire FROM pf_foyer_settings WHERE id = 1")->fetch();
|
$pdo->exec("SET collation_connection = utf8mb4_general_ci");
|
||||||
if (!defined('CURRENCY')) {
|
$pdo->exec("SET collation_database = utf8mb4_general_ci");
|
||||||
define('CURRENCY', $foyer['currency'] ?? '€');
|
$pdo->exec("SET collation_server = utf8mb4_general_ci");
|
||||||
}
|
|
||||||
if (!defined('ZONE_SCOLAIRE')) {
|
|
||||||
define('ZONE_SCOLAIRE', $foyer['zone_scolaire'] ?? 'C');
|
|
||||||
}
|
|
||||||
} catch (\PDOException $e) {
|
|
||||||
if (!defined('CURRENCY')) define('CURRENCY', '€');
|
|
||||||
if (!defined('ZONE_SCOLAIRE')) define('ZONE_SCOLAIRE', 'C');
|
|
||||||
}
|
|
||||||
// ------------------------------------------------
|
|
||||||
|
|
||||||
} catch (\PDOException $e) {
|
} catch (\PDOException $e) {
|
||||||
if (!headers_sent()) { header('Content-Type: application/json'); http_response_code(500); }
|
die("Erreur de connexion BDD (Host: $host, DB: $db) : " . $e->getMessage());
|
||||||
die(json_encode(['ok' => false, 'error' => 'Erreur BDD : ' . $e->getMessage()]));
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
+554
-190
@@ -1,28 +1,23 @@
|
|||||||
<?php
|
<?php
|
||||||
// Dictionnaire Catalan
|
// Dictionnaire Catalan - HouseHub
|
||||||
return [
|
return [
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// TEST AUTO
|
// 1. GLOBAL, NAVIGATION & ACTIONS
|
||||||
// ==========================================
|
|
||||||
'test_running' => 'Proves en curs...',
|
|
||||||
'test_passed' => 'Prova superada',
|
|
||||||
'test_failed' => 'Prova fallida',
|
|
||||||
'tests_title' => 'Laboratori de Proves',
|
|
||||||
'tests_run_budget' => 'Provar el Pressupost',
|
|
||||||
'tests_run_all' => 'Iniciar totes les proves',
|
|
||||||
'tests_copy_report' => 'Copiar l\'informe',
|
|
||||||
'tests_waiting' => 'Esperant l\'inici...',
|
|
||||||
'tests_report_copied' => 'Informe copiat al porta-retalls!',
|
|
||||||
'tests_live_preview' => 'Previsualització en viu',
|
|
||||||
// ==========================================
|
|
||||||
// GLOBAL & NAVIGATION
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'menu_home' => 'Inici',
|
'menu_home' => 'Inici',
|
||||||
'menu_calendar' => 'Calendari',
|
'menu_calendar' => 'Calendari',
|
||||||
'menu_budget' => 'Pressupost',
|
'menu_budget' => 'Pressupost',
|
||||||
'menu_holidays' => 'Viatges',
|
'menu_holidays' => 'Viatges',
|
||||||
'menu_gifts' => 'Regals',
|
'menu_gifts' => 'Regals',
|
||||||
|
'menu_garage' => 'Garatge',
|
||||||
|
'menu_memo' => 'Notes',
|
||||||
|
'menu_todo' => 'Todo',
|
||||||
|
'menu_liste' => 'Llistes',
|
||||||
|
'menu_groceries' => 'Compra',
|
||||||
'menu_calendar_ios' => 'Calendari iOS',
|
'menu_calendar_ios' => 'Calendari iOS',
|
||||||
|
'menu_printvault' => 'PrintVault',
|
||||||
|
'menu_planka' => 'Planka',
|
||||||
|
|
||||||
'nav_home' => 'Inici',
|
'nav_home' => 'Inici',
|
||||||
'nav_calendar' => 'Calendari',
|
'nav_calendar' => 'Calendari',
|
||||||
'nav_budget' => 'Pressupost',
|
'nav_budget' => 'Pressupost',
|
||||||
@@ -30,7 +25,6 @@ return [
|
|||||||
'nav_gifts' => 'Regals',
|
'nav_gifts' => 'Regals',
|
||||||
'aria_menu' => 'Menú mòbil',
|
'aria_menu' => 'Menú mòbil',
|
||||||
|
|
||||||
// Boutons et actions globales
|
|
||||||
'actions' => 'Accions',
|
'actions' => 'Accions',
|
||||||
'edit' => 'Modificar',
|
'edit' => 'Modificar',
|
||||||
'delete' => 'Suprimir',
|
'delete' => 'Suprimir',
|
||||||
@@ -40,28 +34,33 @@ return [
|
|||||||
'btn_cancel' => 'Cancel·lar',
|
'btn_cancel' => 'Cancel·lar',
|
||||||
'btn_delete' => 'Eliminar',
|
'btn_delete' => 'Eliminar',
|
||||||
'btn_close' => 'Tancar',
|
'btn_close' => 'Tancar',
|
||||||
|
'btn_add' => 'Afegir',
|
||||||
'btn_back' => '◀ Tornar',
|
'btn_back' => '◀ Tornar',
|
||||||
'btn_edit_bases' => '⚙️ Modificar les bases',
|
'btn_edit_bases' => '⚙️ Modificar les bases',
|
||||||
'btn_edit' => 'Modificar',
|
'btn_edit' => 'Modificar',
|
||||||
'error_occured' => 'S\'ha produït un error.',
|
|
||||||
|
|
||||||
// Mots génériques & Erreurs
|
|
||||||
'error' => 'Error.',
|
'error' => 'Error.',
|
||||||
|
'error_occured' => 'S\'ha produït un error.',
|
||||||
|
'error_network' => 'Error de xarxa',
|
||||||
|
'error_generic' => 'S\'ha produït un error',
|
||||||
'confirm_delete' => 'Estàs segur que vols suprimir aquest element?',
|
'confirm_delete' => 'Estàs segur que vols suprimir aquest element?',
|
||||||
|
'loading' => 'Carregant...',
|
||||||
|
'date' => 'Data',
|
||||||
|
'settings' => 'Configuració',
|
||||||
|
|
||||||
// Mois & Jours
|
// Mesos & Dies
|
||||||
'month_01' => 'Gener', 'month_02' => 'Febrer', 'month_03' => 'Març',
|
'month_01' => 'Gener', 'month_02' => 'Febrer', 'month_03' => 'Març', 'month_04' => 'Abril', 'month_05' => 'Maig', 'month_06' => 'Juny',
|
||||||
'month_04' => 'Abril', 'month_05' => 'Maig', 'month_06' => 'Juny',
|
'month_07' => 'Juliol','month_08' => 'Agost', 'month_09' => 'Setembre', 'month_10' => 'Octubre','month_11' => 'Novembre','month_12' => 'Desembre',
|
||||||
'month_07' => 'Juliol','month_08' => 'Agost', 'month_09' => 'Setembre',
|
'month_jan' => 'Gener', 'month_feb' => 'Febrer', 'month_mar' => 'Març', 'month_apr' => 'Abril', 'month_may' => 'Maig', 'month_jun' => 'Juny',
|
||||||
'month_10' => 'Octubre','month_11' => 'Novembre','month_12' => 'Desembre',
|
'month_jul' => 'Juliol', 'month_aug' => 'Agost', 'month_sep' => 'Setembre', 'month_oct' => 'Octubre', 'month_nov' => 'Novembre', 'month_dec' => 'Desembre',
|
||||||
'day_mon' => 'Dl', 'day_tue' => 'Dt', 'day_wed' => 'Dc',
|
'day_mon' => 'Dl', 'day_tue' => 'Dt', 'day_wed' => 'Dc', 'day_thu' => 'Dj', 'day_fri' => 'Dv', 'day_sat' => 'Ds', 'day_sun' => 'Dg',
|
||||||
'day_thu' => 'Dj', 'day_fri' => 'Dv', 'day_sat' => 'Ds', 'day_sun' => 'Dg',
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// AUTHENTIFICATION / LOGIN
|
// 2. AUTHENTICATION / LOGIN
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'login_title' => 'Connexió - HouseHub',
|
'login_title' => 'Connexió - HouseHub',
|
||||||
'login_header' => 'Connexió',
|
'login_header' => 'Connexió',
|
||||||
|
'login_subtitle' => 'Si us plau, identifiqueu-vos per continuar',
|
||||||
'label_username' => "Nom d'usuari",
|
'label_username' => "Nom d'usuari",
|
||||||
'label_password' => 'Contrasenya',
|
'label_password' => 'Contrasenya',
|
||||||
'placeholder_username' => 'ex: pacha',
|
'placeholder_username' => 'ex: pacha',
|
||||||
@@ -69,18 +68,19 @@ return [
|
|||||||
'btn_login_submit' => 'Connectar-se',
|
'btn_login_submit' => 'Connectar-se',
|
||||||
'logout' => 'Tancar sessió',
|
'logout' => 'Tancar sessió',
|
||||||
'btn_logout' => 'Tancar sessió',
|
'btn_logout' => 'Tancar sessió',
|
||||||
|
'login_remember_me' => "Recorda'm",
|
||||||
'error_missing_fields' => 'Camps obligatoris.',
|
'error_missing_fields' => 'Camps obligatoris.',
|
||||||
'error_invalid_credentials' => 'Identificadors incorrectes.',
|
'error_invalid_credentials' => 'Identificadors incorrectes.',
|
||||||
'login_subtitle' => 'Si us plau, identifiqueu-vos per continuar',
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// ACCUEIL / INDEX
|
// 3. HOME & DASHBOARD
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'home_title' => 'HouseHub - Inici',
|
'home_title' => 'HouseHub - Inici',
|
||||||
'home_welcome' => 'Benvinguts a HouseHub',
|
'home_welcome' => 'Benvinguts a HouseHub',
|
||||||
'home_subtitle' => "Centre de control de l'organització familiar.",
|
'home_subtitle' => "Centre de control de l'organització familiar.",
|
||||||
'home_logged_as' => 'Connectat com a',
|
'home_logged_as' => 'Connectat com a',
|
||||||
'home_modules_title' => 'Mòduls',
|
'home_modules_title' => 'Mòduls',
|
||||||
|
|
||||||
'mod_calendar_name' => 'Calendari Familiar',
|
'mod_calendar_name' => 'Calendari Familiar',
|
||||||
'mod_calendar_desc' => "Gestioneu les vacances, les modalitats de cura i l'organització setmanal.",
|
'mod_calendar_desc' => "Gestioneu les vacances, les modalitats de cura i l'organització setmanal.",
|
||||||
'mod_holidays_name' => 'Vacances',
|
'mod_holidays_name' => 'Vacances',
|
||||||
@@ -93,52 +93,19 @@ return [
|
|||||||
'mod_garage_desc' => 'Seguiu el manteniment dels vostres vehicles, les peces i els costos.',
|
'mod_garage_desc' => 'Seguiu el manteniment dels vostres vehicles, les peces i els costos.',
|
||||||
'mod_memo_name' => 'Notes',
|
'mod_memo_name' => 'Notes',
|
||||||
'mod_memo_desc' => 'Els vostres memos, receptes, instruccions i informació diversa amb Markdown.',
|
'mod_memo_desc' => 'Els vostres memos, receptes, instruccions i informació diversa amb Markdown.',
|
||||||
'menu_memo' => 'Notes',
|
|
||||||
'mod_todo_name' => 'Todo',
|
'mod_todo_name' => 'Todo',
|
||||||
'mod_todo_desc' => 'Gestioneu les vostres tasques, llistes de la compra i recordatoris familiars.',
|
'mod_todo_desc' => 'Gestioneu les vostres tasques, llistes de la compra i recordatoris familiars.',
|
||||||
'mod_calendar_ios_name' => 'Calendari iOS',
|
'mod_liste_name' => 'Llistes',
|
||||||
'mod_calendar_ios_desc' => 'Sincronitzeu els esdeveniments de HouseHub amb el calendari de l\'iPhone (iCloud CalDAV).',
|
'mod_liste_desc' => 'Llistes compartides — compres, bricolatge, farmàcia… Tantes llistes com convingui.',
|
||||||
'menu_todo' => 'Todo',
|
|
||||||
'mod_groceries_name' => 'Compra',
|
'mod_groceries_name' => 'Compra',
|
||||||
'mod_groceries_desc' => 'Llista compartida: afegiu productes i marqueu-los quan siguin al carretó.',
|
'mod_groceries_desc' => 'Llista compartida: afegiu productes i marqueu-los quan siguin al carretó.',
|
||||||
'menu_groceries' => 'Compra',
|
'mod_calendar_ios_name' => 'Calendari iOS',
|
||||||
'groceries_placeholder_add' => 'Ex : llet, pa, tomàquets…',
|
'mod_calendar_ios_desc' => 'Sincronitzeu els esdeveniments de HouseHub amb el calendari de l\'iPhone (iCloud CalDAV).',
|
||||||
'groceries_btn_add' => 'Afegir',
|
'mod_printvault_name'=> 'PrintVault',
|
||||||
'groceries_btn_uncheck_all' => 'Tornar a marcar com a pendents',
|
'mod_printvault_desc'=> 'Gestioneu els vostres fitxers d\'impressió 3D — STL, 3MF, GCode.',
|
||||||
'groceries_btn_delete_picked' => 'Eliminar els marcats',
|
'mod_planka_name' => 'Planka',
|
||||||
'groceries_subtitle' => '{pending} pendents · {cart} al carretó',
|
'mod_planka_desc' => 'Gestioneu els vostres projectes en mode Kanban amb Planka.',
|
||||||
'groceries_added' => 'Afegit',
|
|
||||||
'groceries_updated' => 'Actualitzat',
|
|
||||||
'groceries_deleted' => 'Eliminat',
|
|
||||||
'groceries_reset_next' => 'Llista llesta per a la propera compra',
|
|
||||||
'groceries_picked_removed' => 'Articles eliminats',
|
|
||||||
'groceries_label_empty' => 'El text no pot ser buit',
|
|
||||||
'groceries_confirm_del_title' => 'Eliminar?',
|
|
||||||
'groceries_confirm_del_body' => 'Voleu treure aquest producte de la llista?',
|
|
||||||
'groceries_confirm_uncheck_title' => 'Tornar a marcar com a pendents',
|
|
||||||
'groceries_confirm_uncheck_body' => 'Es netejaran les marques del carretó. Els productes es mantindran a la llista.',
|
|
||||||
'groceries_confirm_clear_title' => 'Eliminar els marcats',
|
|
||||||
'groceries_confirm_clear_body' => 'Tots els articles marcats se suprimiran de la llista.',
|
|
||||||
'groceries_empty' => 'Encara no hi ha res. Afegiu un producte a dalt.',
|
|
||||||
'groceries_aria_toggle' => 'Marcar com al carretó o pendent',
|
|
||||||
'groceries_section_pending' => 'Pendents',
|
|
||||||
'groceries_section_cart' => 'Al carretó',
|
|
||||||
'groceries_prompt_edit' => 'Editar el producte',
|
|
||||||
'groceries_btn_clear_list' => 'Buidar la llista',
|
|
||||||
'groceries_confirm_empty_list_title' => 'Buidar tota la llista?',
|
|
||||||
'groceries_confirm_empty_list_body' => 'Es trauran tots els productes. Romandran a l’historial per tornar-los a afegir ràpidament.',
|
|
||||||
'groceries_list_cleared' => 'Llista buidada',
|
|
||||||
'groceries_history_title' => 'Historial — un clic per afegir',
|
|
||||||
'groceries_history_empty' => 'L’historial es va omplint amb els productes que afegiu (fins al límit definit als paràmetres).',
|
|
||||||
'groceries_history_meta' => 'Fins a {max} productes · ',
|
|
||||||
'groceries_settings_link' => 'Paràmetres',
|
|
||||||
'groceries_settings_title' => 'Compra — historial',
|
|
||||||
'groceries_settings_intro' => 'Nombre màxim de productes recordats a l’historial (sense duplicats, sense distingir majúscules).',
|
|
||||||
'groceries_settings_history_max' => 'Mida de l’historial',
|
|
||||||
'groceries_settings_history_hint' => 'Entre 1 i 50. Les entrades més antigues se suprimeixen quan es supera el límit.',
|
|
||||||
'groceries_settings_saved' => 'Preferències de la compra desades.',
|
|
||||||
'groceries_settings_error' => 'No s’ha pogut desar:',
|
|
||||||
'groceries_err_duplicate' => 'Aquest producte ja és a la llista.',
|
|
||||||
'cta_open' => 'Obrir',
|
'cta_open' => 'Obrir',
|
||||||
'cta_explore' => 'Explorar',
|
'cta_explore' => 'Explorar',
|
||||||
'cta_view_lists' => 'Desembolicar',
|
'cta_view_lists' => 'Desembolicar',
|
||||||
@@ -148,16 +115,123 @@ return [
|
|||||||
'cta_check' => 'Marcar',
|
'cta_check' => 'Marcar',
|
||||||
'cta_cart' => 'Anar a comprar',
|
'cta_cart' => 'Anar a comprar',
|
||||||
'cta_sync' => 'Sincronitzar',
|
'cta_sync' => 'Sincronitzar',
|
||||||
'mod_printvault_name' => 'PrintVault',
|
|
||||||
'mod_printvault_desc' => 'Gestioneu els vostres fitxers d\'impressió 3D — STL, 3MF, GCode.',
|
|
||||||
'menu_printvault' => 'PrintVault',
|
|
||||||
'cta_print' => 'Imprimir',
|
'cta_print' => 'Imprimir',
|
||||||
'mod_planka_name' => 'Planka',
|
|
||||||
'mod_planka_desc' => 'Gestioneu els vostres projectes en mode Kanban amb Planka.',
|
|
||||||
'menu_planka' => 'Planka',
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// FAMILY CALENDAR
|
// 4. SETTINGS
|
||||||
|
// ==========================================
|
||||||
|
'set_title_page' => '⚙️ Configuració',
|
||||||
|
'set_title_lang' => '🌐 Idioma',
|
||||||
|
'set_title_modules' => '🧩 Mòduls Actius',
|
||||||
|
'set_desc_modules' => 'Tria els mòduls visibles a la navegació (compartits amb tots els membres de l\'espai).',
|
||||||
|
'set_title_ios' => '📱 Integració de Calendari iOS (CalDAV)',
|
||||||
|
'set_desc_ios' => 'Configura aquí el teu calendari iCloud per sincronitzar els esdeveniments creats a HouseHub.',
|
||||||
|
'set_label_apple_id' => 'ID d\'Apple (correu iCloud)',
|
||||||
|
'set_label_apple_pwd' => 'Contrasenya d\'aplicació d\'Apple',
|
||||||
|
'set_label_apple_url' => 'URL del calendari CalDAV',
|
||||||
|
'set_btn_test_ios' => 'Prova la connexió',
|
||||||
|
'set_btn_disconnect_ios' => 'Desconnecta',
|
||||||
|
'set_title_bg' => '🖼️ Imatge d\'Inici',
|
||||||
|
'set_desc_bg' => 'Foto de fons mostrada a la pàgina d\'inici (compartida amb tots els membres).',
|
||||||
|
'set_badge_custom_bg' => 'Imatge personalitzada',
|
||||||
|
'set_label_new_bg' => 'Nova imatge (jpg, png, webp — màx 10 MB)',
|
||||||
|
'set_btn_reset_bg' => '🔄 Restableix la imatge per defecte',
|
||||||
|
'set_confirm_reset_bg' => 'Tornar a la imatge per defecte?',
|
||||||
|
'set_title_profile' => '👤 El meu Perfil',
|
||||||
|
'set_label_username' => 'Nom d\'usuari',
|
||||||
|
'set_label_display_name' => 'Nom / Àlies mostrat',
|
||||||
|
'set_label_profile_color' => 'Color temàtic del meu perfil',
|
||||||
|
'set_desc_profile_color' => 'Aquest color s\'utilitzarà per a les teves graelles de pressupost, calendaris i indicadors personals.',
|
||||||
|
'set_title_pwd' => '🔒 Canviar Contrasenya',
|
||||||
|
'set_label_cur_pwd' => 'Contrasenya actual',
|
||||||
|
'set_label_new_pwd' => 'Nova contrasenya',
|
||||||
|
'set_label_confirm_pwd' => 'Confirma',
|
||||||
|
'set_btn_edit' => 'Modifica',
|
||||||
|
'set_title_family' => '🏠 El meu Espai Familiar',
|
||||||
|
'set_label_family_name' => 'Nom de l\'espai',
|
||||||
|
'set_desc_invite' => 'Codi d\'invitació — comparteix-lo per convidar algú',
|
||||||
|
'set_title_copy' => 'Fes clic per copiar',
|
||||||
|
'set_msg_copied' => '✓ Copiat!',
|
||||||
|
'set_btn_new_code' => '🔄 Nou codi',
|
||||||
|
'set_confirm_new_code' => 'Regenerar codi? L\'antic serà invalidat.',
|
||||||
|
'set_desc_register_link' => 'Enllaç de registre:',
|
||||||
|
'set_title_members' => 'Membres',
|
||||||
|
'set_badge_admin' => 'Admin',
|
||||||
|
'set_title_attached' => '🧸 Perfils Adjunts',
|
||||||
|
'set_desc_attached' => 'Membres gestionats per la llar sense accés directe.',
|
||||||
|
'set_empty_attached' => 'Cap perfil adjunt.',
|
||||||
|
'set_link_admin' => '→ Tauler d\'Administració',
|
||||||
|
'set_kids_title' => 'Nens de la llar',
|
||||||
|
'set_kids_desc' => 'Gestioneu la llista de nens per associar-los a l\'estalvi, als regals, etc.',
|
||||||
|
'set_add_kid_placeholder' => 'Nom del nen',
|
||||||
|
'set_btn_add_kid' => 'Afegir',
|
||||||
|
'set_err_kid_name' => 'El nom és obligatori.',
|
||||||
|
'set_confirm_del_kid' => 'Vols eliminar aquest nen? (El seu historial financer es conservarà als arxius).',
|
||||||
|
'set_no_kids' => 'Cap nen configurat per a aquesta llar.',
|
||||||
|
'set_family_config' => 'Configuració de la llar',
|
||||||
|
'set_global_params' => 'Paràmetres globals',
|
||||||
|
'set_care_modes' => 'Modalitats de cura habituals',
|
||||||
|
'set_add_care_mode' => 'Afegir una modalitat',
|
||||||
|
|
||||||
|
// Labels Profiles
|
||||||
|
'label_firstname' => 'Nom',
|
||||||
|
'placeholder_firstname_ex' => 'Ex: Pol, Carole...',
|
||||||
|
'label_role' => 'Rol',
|
||||||
|
'role_child' => '👶 Nen/a',
|
||||||
|
'role_child_icon' => '👶 Nen/a',
|
||||||
|
'role_helper' => '💼 Ajudant',
|
||||||
|
'role_helper_icon' => '💼 Ajudant',
|
||||||
|
'role_relative' => '👵 Familiar',
|
||||||
|
'role_relative_icon' => '👵 Familiar',
|
||||||
|
'role_unknown' => 'Desconegut',
|
||||||
|
'label_color' => 'Color',
|
||||||
|
'change_color' => 'Canviar el color',
|
||||||
|
'confirm_delete_profile' => 'Voleu eliminar definitivament aquest perfil?',
|
||||||
|
|
||||||
|
// Settings Messages
|
||||||
|
'error_csrf' => 'Sessió invàlida (CSRF). Si us plau, recarrega la pàgina.',
|
||||||
|
'set_err_min_module' => 'Has de mantenir almenys un mòdul actiu.',
|
||||||
|
'set_success_modules' => 'Mòduls actualitzats amb èxit.',
|
||||||
|
'set_success_lang' => 'Idioma actualitzat.',
|
||||||
|
'set_err_empty_name' => 'El nom no pot estar buit.',
|
||||||
|
'set_success_profile' => 'Perfil actualitzat amb èxit.',
|
||||||
|
'set_err_wrong_pwd' => 'Contrasenya actual incorrecta.',
|
||||||
|
'set_err_pwd_length' => 'La nova contrasenya ha de tenir almenys 6 caràcters.',
|
||||||
|
'set_err_pwd_match' => 'Les contrasenyes no coincideixen.',
|
||||||
|
'set_success_pwd' => 'Contrasenya canviada amb èxit.',
|
||||||
|
'set_success_family' => 'Nom de l\'espai actualitzat.',
|
||||||
|
'set_success_invite' => 'Nou codi d\'invitació generat.',
|
||||||
|
'set_err_bg_format' => 'Format no compatible (jpg, png, webp).',
|
||||||
|
'set_success_bg' => 'Imatge d\'inici actualitzada.',
|
||||||
|
'set_err_upload' => 'Error durant la càrrega.',
|
||||||
|
'set_success_bg_reset' => 'Imatge d\'inici restablerta.',
|
||||||
|
'set_err_db_unavailable' => 'Base de dades familiar no disponible.',
|
||||||
|
'set_err_ios_missing' => 'Si us plau, proporciona un ID d\'iCloud, contrasenya d\'aplicació i URL CalDAV.',
|
||||||
|
'set_success_ios' => 'Connexió de calendari iOS desada.',
|
||||||
|
'set_success_ios_auto' => ' URL del calendari detectada automàticament.',
|
||||||
|
'set_err_ios_save' => 'No es pot desar la connexió iOS: ',
|
||||||
|
'set_err_ios_none' => 'Cap connexió iOS configurada.',
|
||||||
|
'set_success_ios_test' => 'Connexió iCloud CalDAV vàlida (HTTP %s).',
|
||||||
|
'set_err_ios_test' => 'La prova de connexió ha fallat (HTTP %s). Revisa les teves credencials.',
|
||||||
|
'set_err_ios_test_fail' => 'Prova de connexió no disponible: ',
|
||||||
|
'set_success_ios_del' => 'Connexió iOS eliminada.',
|
||||||
|
'set_success_profile_add' => 'Perfil de %s afegit amb èxit.',
|
||||||
|
'set_err_name_required' => 'El nom és obligatori.',
|
||||||
|
'set_success_profile_del' => 'Perfil suprimit.',
|
||||||
|
'set_success_color' => 'Color actualitzat!',
|
||||||
|
'set_success_color_profile' => 'Color del perfil actualitzat.',
|
||||||
|
'set_success_color_update' => 'Color actualitzat!',
|
||||||
|
'color_updated' => 'Color actualitzat!',
|
||||||
|
'settings_profile_added' => 'Perfil de %s afegit amb èxit.',
|
||||||
|
'settings_err_name_required' => 'El nom és obligatori.',
|
||||||
|
'settings_attached_profiles_title' => 'Perfils adjunts',
|
||||||
|
'settings_attached_profiles_desc' => 'Membres de la llar sense accés directe.',
|
||||||
|
'settings_no_attached_profiles' => 'Cap perfil adjunt.',
|
||||||
|
'placeholder_apple_id' => 'nom@icloud.com',
|
||||||
|
'placeholder_apple_pwd' => 'xxxx-xxxx-xxxx-xxxx',
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 5. FAMILY CALENDAR
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'fc_page_title' => 'HouseHub - Calendari',
|
'fc_page_title' => 'HouseHub - Calendari',
|
||||||
'fc_main_header' => 'Calendari Familiar',
|
'fc_main_header' => 'Calendari Familiar',
|
||||||
@@ -182,6 +256,7 @@ return [
|
|||||||
'fc_weekly_planning' => 'Planificació setmanal',
|
'fc_weekly_planning' => 'Planificació setmanal',
|
||||||
'fc_col_month' => 'Mes',
|
'fc_col_month' => 'Mes',
|
||||||
'fc_col_week_short' => 'Set.',
|
'fc_col_week_short' => 'Set.',
|
||||||
|
'fc_col_week' => 'Setmana',
|
||||||
'fc_col_av' => 'Av.',
|
'fc_col_av' => 'Av.',
|
||||||
'fc_col_use' => 'Use',
|
'fc_col_use' => 'Use',
|
||||||
'fc_legend_title' => 'Llegenda',
|
'fc_legend_title' => 'Llegenda',
|
||||||
@@ -192,14 +267,98 @@ return [
|
|||||||
'fc_unit_days' => 'dies',
|
'fc_unit_days' => 'dies',
|
||||||
'fc_menu_carole' => 'Permisos Carole',
|
'fc_menu_carole' => 'Permisos Carole',
|
||||||
'fc_clear' => 'Esborrar',
|
'fc_clear' => 'Esborrar',
|
||||||
'fc_menu_kids_leaves' => 'Permisos nens',
|
'fc_menu_kids_leaves' => 'Permisos pares',
|
||||||
'fc_err_save_snap' => 'Error en desar el correctiu.',
|
'fc_err_save_snap' => 'Error en desar el correctiu.',
|
||||||
'fc_err_fetch_gov' => 'Error en la connexió amb l\'API governamental.',
|
'fc_err_fetch_gov' => 'Error en la connexió amb l\'API governamental.',
|
||||||
'fc_err_no_data_gov' => 'No s\'han trobat dades a l\'API del govern per a aquest any.',
|
'fc_err_no_data_gov' => 'No s\'han trobat dades a l\'API del govern per a aquest any.',
|
||||||
'fc_err_action' => 'Error en l\'acció: ',
|
'fc_err_action' => 'Error en l\'acció: ',
|
||||||
|
'fc_err_select_leave' => 'Si us plau, selecciona un tipus de permís.',
|
||||||
'fc_today_short' => 'Avui',
|
'fc_today_short' => 'Avui',
|
||||||
'fc_today_title' => 'Anar a avui',
|
'fc_today_title' => 'Anar a avui',
|
||||||
|
'fc_tab_foyer' => '🏡 Llar',
|
||||||
|
'fc_tab_members' => '👥 Membres i Drets',
|
||||||
|
'fc_select_member' => 'Selecciona un membre de la família:',
|
||||||
|
'fc_role_adult' => 'Adult',
|
||||||
|
'fc_role_child' => 'Nen',
|
||||||
|
'fc_role_helper' => 'Ajudant',
|
||||||
|
'fc_role_unknown' => 'Desconegut',
|
||||||
|
'fc_care_modes_title' => 'Modes de Cura Predeterminats',
|
||||||
|
'fc_care_modes_desc' => 'Quins modes de cura s\'apliquen a aquest nen/a?',
|
||||||
|
'fc_no_care_mode' => 'No hi ha modes de cura configurats a la llar.',
|
||||||
|
'fc_matrix_title' => 'Matriu de Drets de Permís',
|
||||||
|
'fc_matrix_desc' => 'Declara els codis de comptadors i les dates de renovació.',
|
||||||
|
'fc_matrix_code' => 'Codi',
|
||||||
|
'fc_matrix_date' => 'Data de Reset',
|
||||||
|
'fc_matrix_empty' => 'No hi ha comptadors definits.',
|
||||||
|
'fc_matrix_saved' => 'Matriu de drets actualitzada! 💼',
|
||||||
|
'fc_child_saved' => 'Desat!',
|
||||||
|
'btn_add_counter' => '➕ Afegir',
|
||||||
|
'btn_save_rights' => 'Guardar',
|
||||||
|
'fc_modal_settings_title' => 'Configuració del Calendari',
|
||||||
|
'fc_zone_label' => 'Zona escolar',
|
||||||
|
'fc_zone_a' => 'Zona A',
|
||||||
|
'fc_zone_b' => 'Zona B',
|
||||||
|
'fc_zone_c' => 'Zona C',
|
||||||
|
'fc_zone_other' => 'Altra / Fora de França',
|
||||||
|
'fc_zone_desc' => 'Permet mostrar correctament les vacances escolars.',
|
||||||
|
'fc_care_modes_label' => 'Modalitats de cura',
|
||||||
|
'fc_add_mode_placeholder' => 'Ex: Mainadera, Llar d\'infants...',
|
||||||
|
'fc_empty_kids_msg' => 'Actualment no hi ha cap infant configurat a la teva família.',
|
||||||
|
'fc_link_settings' => 'Anar a la configuració general',
|
||||||
|
'fc_settings_updated' => 'Configuració actualitzada correctament!',
|
||||||
|
'fc_first_day' => 'Primer dia de la setmana',
|
||||||
|
'fc_helper_recap' => 'Resum Interventor',
|
||||||
|
'fc_no_helper_leaves' => 'Cap permís sol·licitat aquest mes.',
|
||||||
|
'fc_col_date' => 'Data',
|
||||||
|
'fc_col_type' => 'Tipus',
|
||||||
|
'fc_col_duration' => 'Durada',
|
||||||
|
'fc_full_year' => 'Any sencer',
|
||||||
|
'fc_custom_period' => 'Període específic...',
|
||||||
|
'fc_worked_days' => 'Treballats (Est.)',
|
||||||
|
'fc_tab_display' => '🖥️ Visualització',
|
||||||
|
'fc_display_title' => 'Ajustos de visualització',
|
||||||
|
'fc_display_desc' => 'Personalitza l\'aparença i el comportament predeterminat del teu calendari.',
|
||||||
|
'fc_default_view' => 'Vista per defecte',
|
||||||
|
'fc_working_hours' => 'Horari de cura (Per defecte)',
|
||||||
|
'fc_working_hours_desc' => 'Exemple: 08:30-18:00',
|
||||||
|
'fc_error' => 'Error: ',
|
||||||
|
'fc_leave_catalog_title' => 'Catàleg de permisos',
|
||||||
|
'fc_leave_catalog_desc' => 'Defineix els tipus de permisos possibles per a tota la família.',
|
||||||
|
'fc_add_leave_type' => 'Afegir un tipus de permís',
|
||||||
|
'fc_col_code' => 'Codi',
|
||||||
|
'fc_code_irreversible' => 'Irreversible',
|
||||||
|
'fc_col_label' => 'Etiqueta',
|
||||||
|
'fc_leave_label_ph' => 'Ex: Vacances, Assumptes propis...',
|
||||||
|
'fc_quota_default' => 'Quota per defecte',
|
||||||
|
'fc_reset_month' => 'Mes de renovació',
|
||||||
|
'fc_leave_obsolete' => 'Obsolet',
|
||||||
|
'fc_leave_code_placeholder' => 'Codi...',
|
||||||
|
'fc_leave_method_fixed' => 'Fix',
|
||||||
|
'fc_leave_method_accumulated'=> 'Acumulat',
|
||||||
|
'fc_date_format_placeholder'=> 'DD/MM',
|
||||||
|
'fc_date_format_title' => 'Format DD/MM',
|
||||||
|
'fc_table_code' => 'Codi',
|
||||||
|
'fc_table_method' => 'Mètode',
|
||||||
|
'fc_table_quota' => 'Quota',
|
||||||
|
'fc_table_renewal' => 'Renov.',
|
||||||
|
'fc_label_carry_over' => 'Inclou dies reportats (Caducables) ⚠️',
|
||||||
|
'fc_carry_over_note' => 'Part del saldo global que caducarà a la data límit.',
|
||||||
|
'fc_carry_max' => 'Màx Reportable',
|
||||||
|
'fc_carry_deadline' => 'Mes de Caducitat',
|
||||||
|
'fc_deadline_none' => 'Cap',
|
||||||
|
'fc_allowance_help' => 'Fix: Quota anual. Gradual: Afegit per mes.',
|
||||||
|
'fc_carry_max_short' => 'Report màx',
|
||||||
|
'fc_edit_leave' => '✏️ Modificar:',
|
||||||
|
'fc_not_modifiable' => 'No modificable',
|
||||||
|
'fc_add_leave' => '+ Afegir',
|
||||||
|
'fc_err_code_label' => 'El Codi i l\'Etiqueta són obligatoris.',
|
||||||
|
'fc_catalog_updated' => 'Catàleg actualitzat!',
|
||||||
|
'fc_balance_corrected' => 'Saldo corregit!',
|
||||||
|
'fc_no_leave_configured' => 'Cap permís configurat.',
|
||||||
|
'fc_confirm_del_catalog' => 'Voleu eliminar definitivament el permís %s del catàleg? Això també el retirarà dels membres que l\'utilitzen.',
|
||||||
|
'fc_confirm_del_member_leave'=> 'Estàs segur que vols retirar aquest permís per a aquest membre?',
|
||||||
|
|
||||||
|
// Llegendes
|
||||||
'leg_presence' => 'Presència Pep',
|
'leg_presence' => 'Presència Pep',
|
||||||
'leg_school_holidays' => 'Vacances Escolars',
|
'leg_school_holidays' => 'Vacances Escolars',
|
||||||
'leg_public_holiday' => 'Festiu',
|
'leg_public_holiday' => 'Festiu',
|
||||||
@@ -207,8 +366,10 @@ return [
|
|||||||
'leg_extra_off' => 'Extra Off',
|
'leg_extra_off' => 'Extra Off',
|
||||||
'leg_centre' => 'Casal',
|
'leg_centre' => 'Casal',
|
||||||
'leg_avis' => 'Avís',
|
'leg_avis' => 'Avís',
|
||||||
'leg_pep_sick' => 'Pep Malalt',
|
'fc_menu_sick' => 'Malalt',
|
||||||
|
'care_nounou' => 'Kangur',
|
||||||
|
'care_centre' => 'Casal',
|
||||||
|
'care_avis' => 'Avis',
|
||||||
'vac_toussaint' => 'Vacances de Tots Sants',
|
'vac_toussaint' => 'Vacances de Tots Sants',
|
||||||
'vac_noel' => 'Vacances de Nadal',
|
'vac_noel' => 'Vacances de Nadal',
|
||||||
'vac_hiver' => "Vacances d'Hivern",
|
'vac_hiver' => "Vacances d'Hivern",
|
||||||
@@ -217,12 +378,11 @@ return [
|
|||||||
'vac_ete' => "Vacances d'Estiu",
|
'vac_ete' => "Vacances d'Estiu",
|
||||||
'btn_off' => 'Off',
|
'btn_off' => 'Off',
|
||||||
'btn_extra' => 'Extra',
|
'btn_extra' => 'Extra',
|
||||||
|
'fc_alert_burn_days' => '⚠️ Risc de pèrdua : %s d a gastar abans del final del cicle',
|
||||||
'fc_alert_burn_days' => '⚠️ Risc de pèrdua : %s d a gastar abans del %s',
|
'fc_alert_burn_jra' => '⚠️ JRA: Només 2d es poden reportar després de febrer!',
|
||||||
'fc_alert_burn_jra' => '⚠️ JRA: Només 2d es poden reportar després de febrer!',
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// HOLIDAYS (VOYAGES)
|
// 6. HOLIDAYS (VOYAGES)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'hdl_title_list' => 'HouseHub - Les meves vacances',
|
'hdl_title_list' => 'HouseHub - Les meves vacances',
|
||||||
'hdl_title_detail' => 'HouseHub - Detall del viatge',
|
'hdl_title_detail' => 'HouseHub - Detall del viatge',
|
||||||
@@ -297,24 +457,21 @@ return [
|
|||||||
'hdl_to_place' => 'Per situar',
|
'hdl_to_place' => 'Per situar',
|
||||||
'hdl_from' => 'Des del',
|
'hdl_from' => 'Des del',
|
||||||
'hdl_to' => 'Fins al',
|
'hdl_to' => 'Fins al',
|
||||||
'hdl_btn_add_step' => 'Afegir una etapa',
|
'weather_forecast' => 'Previsions meteorològiques',
|
||||||
'hdl_quick_edit_title' => 'Modificació ràpida',
|
'weather_not_available' => 'Temps no disponible',
|
||||||
'hdl_paid' => 'Pagat',
|
'temp_max' => 'Temp. Màxima',
|
||||||
'weather_forecast' => 'Previsions meteorològiques',
|
'weather_loading' => 'Carregant el temps...',
|
||||||
'weather_not_available' => 'Temps no disponible',
|
'weather_error' => 'Temps no disponible',
|
||||||
'temp_max' => 'Temp. Màxima',
|
'weather_sunny' => 'Assolellat',
|
||||||
'weather_loading' => 'Carregant el temps...',
|
'weather_cloudy' => 'Ennuvolat',
|
||||||
'weather_error' => 'Temps no disponible',
|
'weather_rainy' => 'Plujós',
|
||||||
'weather_sunny' => 'Assolellat',
|
'weather_snowy' => 'Nevat',
|
||||||
'weather_cloudy' => 'Ennuvolat',
|
'weather_historical' => 'Basat en l\'històric (estimació)',
|
||||||
'weather_rainy' => 'Plujós',
|
'err_invalid_holiday_id' => 'ID de viatge no vàlid.',
|
||||||
'weather_snowy' => 'Nevat',
|
'hdl_default_exp_name' => 'Despesa',
|
||||||
'weather_historical' => 'Basat en l\'històric (estimació)',
|
'hdl_select_beneficiary' => '-- Seleccionar --',
|
||||||
'err_invalid_holiday_id' => 'ID de viatge no vàlid.',
|
|
||||||
'hdl_default_exp_name' => 'Despesa',
|
|
||||||
'hdl_select_beneficiary' => '-- Seleccionar --',
|
|
||||||
|
|
||||||
// Anciennes clés Holidays non préfixées (conservades)
|
// Anciennes clés (conservades)
|
||||||
'outbound_trip' => 'Anada',
|
'outbound_trip' => 'Anada',
|
||||||
'return_trip' => 'Tornada',
|
'return_trip' => 'Tornada',
|
||||||
'period' => 'Període',
|
'period' => 'Període',
|
||||||
@@ -367,7 +524,7 @@ return [
|
|||||||
'to_place' => '📥 A col·locar',
|
'to_place' => '📥 A col·locar',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// BUDGET (GLOBAL & CATEGORIES)
|
// 7. BUDGET (GLOBAL, SUIVI, PREV, EPARGNE, PROVISIONS)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'budget_page_title' => 'HouseHub - Pressupost',
|
'budget_page_title' => 'HouseHub - Pressupost',
|
||||||
'budget_main_header' => 'Gestió del Pressupost',
|
'budget_main_header' => 'Gestió del Pressupost',
|
||||||
@@ -375,9 +532,16 @@ return [
|
|||||||
'budget_tab_prev' => 'Pressupost 2026',
|
'budget_tab_prev' => 'Pressupost 2026',
|
||||||
'budget_tab_savings' => 'Estalvis',
|
'budget_tab_savings' => 'Estalvis',
|
||||||
'budget_tab_recap' => 'Resum',
|
'budget_tab_recap' => 'Resum',
|
||||||
|
'budget_tab_provisions' => 'Provisions',
|
||||||
|
'budget_tab_kids' => 'Nens',
|
||||||
'budget_err_file_not_found' => 'Vaja! Fitxer no trobat.',
|
'budget_err_file_not_found' => 'Vaja! Fitxer no trobat.',
|
||||||
'budget_err_file_detail' => 'Comproveu que el fitxer views/%s.php existeix.',
|
'budget_err_file_detail' => 'Comproveu que el fitxer views/%s.php existeix.',
|
||||||
'budget_err_not_authorized' => 'Pàgina no autoritzada.',
|
'budget_err_not_authorized' => 'Pàgina no autoritzada.',
|
||||||
|
'bud_err_tech' => 'S\'ha produït un error tècnic.',
|
||||||
|
'bud_err_server' => 'Error del servidor: ',
|
||||||
|
'bud_err_delete' => 'Error en suprimir.',
|
||||||
|
|
||||||
|
// Categories
|
||||||
'cat_income' => 'Ingressos',
|
'cat_income' => 'Ingressos',
|
||||||
'cat_fmcg' => 'Compres (FMCG)',
|
'cat_fmcg' => 'Compres (FMCG)',
|
||||||
'cat_fuel' => 'Benzina',
|
'cat_fuel' => 'Benzina',
|
||||||
@@ -386,7 +550,104 @@ return [
|
|||||||
'cat_others' => 'Altres / Imprevistos',
|
'cat_others' => 'Altres / Imprevistos',
|
||||||
'cat_savings' => 'Estalvis',
|
'cat_savings' => 'Estalvis',
|
||||||
|
|
||||||
// --- BUDGET : SUIVI ---
|
// Settings
|
||||||
|
'budget_settings_title' => 'Configuració del Pressupost',
|
||||||
|
'bs_tab_accounts' => '💳 Comptes',
|
||||||
|
'bs_tab_categories' => '🏷️ Categories',
|
||||||
|
'bs_tab_rules' => '🤖 Regles d\'importació',
|
||||||
|
'bs_tab_salaries' => '💼 Salaris',
|
||||||
|
'bs_tab_csv' => '📥 Importació CSV',
|
||||||
|
'bs_empty_accounts' => 'Cap compte configurat.',
|
||||||
|
'bs_type_checking' => 'Corrent',
|
||||||
|
'bs_type_savings' => 'Estalvi',
|
||||||
|
'bs_badge_default' => 'Per defecte',
|
||||||
|
'bs_owner' => 'Propietari',
|
||||||
|
'bs_shared_account' => 'Compte conjunt',
|
||||||
|
'bs_empty_categories' => 'Cap categoria configurada.',
|
||||||
|
'bs_type_income' => 'Ingrés',
|
||||||
|
'bs_type_expense' => 'Despesa',
|
||||||
|
'bs_type_savings_cat' => 'Estalvi',
|
||||||
|
'bs_empty_rules' => 'Cap regla d\'importació.',
|
||||||
|
'bs_empty_salaries' => 'Cap salari definit per a l\'any',
|
||||||
|
'bs_salaries_year_desc' => 'Configuracions per a l\'any',
|
||||||
|
'bs_salary_net' => 'Salari net',
|
||||||
|
'bs_salary_fees' => 'Mensualitat / Despeses',
|
||||||
|
'bs_add_account_title' => 'Afegir un compte',
|
||||||
|
'bs_account_name_ph' => 'Nom (ex: Estalvi Bebè)',
|
||||||
|
'bs_confirm_delete' => 'Segur que voleu suprimir aquest compte?',
|
||||||
|
'bs_add_category_title' => '+ Afegir una categoria',
|
||||||
|
'bs_cat_code_ph' => 'Codi (ex: FOOD)',
|
||||||
|
'bs_cat_label_ph' => 'Etiqueta (ex: Compres)',
|
||||||
|
'bs_cat_icon_ph' => 'Icona (ex: 🛒)',
|
||||||
|
'bs_confirm_delete_cat' => 'Segur que voleu suprimir aquesta categoria?',
|
||||||
|
'bs_add_rule_title' => 'Afegir una regla',
|
||||||
|
'bs_rule_keyword_ph' => 'Paraula clau (ex: AUCHAN)',
|
||||||
|
'bs_confirm_delete_rule'=> 'Segur que voleu suprimir aquesta regla?',
|
||||||
|
'bs_currency_label' => 'Divisa de la llar',
|
||||||
|
'bs_currency_desc' => 'Divisa principal utilitzada per mostrar els vostres comptes i pressupostos.',
|
||||||
|
'bs_currency_updated' => 'Divisa actualitzada correctament!',
|
||||||
|
'bud_settings_dyn_title' => 'Estimacions Dinàmiques (Nens)',
|
||||||
|
'bud_dyn_nanny_fixed' => 'Sou fix Cangur (€)',
|
||||||
|
'bud_dyn_nanny_daily' => 'Despeses diàries de manteniment (€)',
|
||||||
|
'bud_dyn_nanny_aid' => 'Ajuda del govern (€)',
|
||||||
|
'bud_dyn_nanny_cesu_avg' => 'Mitjana CESU pagada (€)',
|
||||||
|
'bud_dyn_school_meal' => 'Preu del menjador (€)',
|
||||||
|
'bud_dyn_school_aftercare' => 'Preu acollida tarda (€)',
|
||||||
|
'bud_dyn_school_fullday' => 'Preu acollida dia sencer (Dimecres/Vacances) (€)',
|
||||||
|
'bud_dyn_pattern_title' => 'Setmana escolar tipus',
|
||||||
|
'bud_audit_month' => 'Mes analitzat: %02d/%d',
|
||||||
|
'bud_audit_nanny_abs' => 'Absències %s deduïdes : %dd',
|
||||||
|
'bud_audit_child' => '👦 %s :',
|
||||||
|
'bud_audit_school_fee' => 'Menjador (%dd) + Acollida (%dd) + Casal (%dd) = %.2f€',
|
||||||
|
'bud_audit_sick' => 'Dies de malaltia deduïts : %dd',
|
||||||
|
'bud_audit_nanny_full' => '💼 %s : Fix (%.2f€) + Despeses (%dd x %.2f€ = %.2f€) - Ajut (%.2f€) - CESU (%.2f€) = %.2f€',
|
||||||
|
|
||||||
|
// CSV
|
||||||
|
'bs_csv_title' => 'Format de la importació CSV',
|
||||||
|
'bs_csv_desc' => 'Configureu aquí les columnes que corresponen a l\'exportació del vostre banc.',
|
||||||
|
'bs_csv_delimiter' => 'Separador',
|
||||||
|
'bs_csv_delim_semi' => 'Punt i coma (;)',
|
||||||
|
'bs_csv_delim_comma' => 'Coma (,)',
|
||||||
|
'bs_csv_delim_tab' => 'Tabulació',
|
||||||
|
'bs_csv_date_format' => 'Format de la Data',
|
||||||
|
'bs_csv_col_index' => 'Índex de les columnes',
|
||||||
|
'bs_csv_col_index_help' => '(Atenció: la 1a columna és la Número 0)',
|
||||||
|
'bs_csv_col_date' => 'Núm Col: Data',
|
||||||
|
'bs_csv_col_label' => 'Núm Col: Concepte / Motiu',
|
||||||
|
'bs_csv_amount_mgmt' => 'Gestió dels imports',
|
||||||
|
'bs_csv_amount_single' => 'Una sola columna (Positiu = Ingrés, Negatiu = Despesa)',
|
||||||
|
'bs_csv_amount_split' => 'Dues columnes separades (Dèbit / Crèdit)',
|
||||||
|
'bs_csv_col_amount' => 'Núm Col: Import',
|
||||||
|
'bs_csv_col_debit' => 'Núm Col: Dèbit',
|
||||||
|
'bs_csv_col_credit' => 'Núm Col: Crèdit',
|
||||||
|
'bs_csv_col_ref' => 'Núm Col: Referència única (Opcional)',
|
||||||
|
'btn_save_format' => 'Desar el format',
|
||||||
|
'bs_csv_saved' => 'Format CSV desat',
|
||||||
|
'bs_csv_settings_title' => 'Configuració del format',
|
||||||
|
'bs_csv_drop_title' => 'Arrossegueu el vostre fitxer CSV aquí',
|
||||||
|
'bs_csv_drop_desc' => 'o feu clic per cercar fitxers',
|
||||||
|
'bs_csv_err_type' => 'Error: Només s\'admeten fitxers .csv.',
|
||||||
|
'bs_csv_file_ok' => 'Fitxer carregat',
|
||||||
|
'bs_csv_preview_title' => 'Previsualització de les vostres columnes',
|
||||||
|
'bs_csv_empty_col' => 'Buit',
|
||||||
|
'bs_csv_no_data' => 'No s\'ha trobat cap línia de transacció en aquest fitxer.',
|
||||||
|
|
||||||
|
// Onboarding
|
||||||
|
'bs_onb_title' => 'Benvinguts! Configurem el teu pressupost:',
|
||||||
|
'bs_onb_desc' => 'Completa aquests passos perquè l\'aplicació pugui calcular el teu pressupost disponible:',
|
||||||
|
'bs_onb_cat_link' => 'Categories',
|
||||||
|
'bs_onb_cat_desc' => 'Crea almenys una categoria de despeses.',
|
||||||
|
'bs_onb_acc_link' => 'Comptes Bancaris',
|
||||||
|
'bs_onb_acc_desc' => 'Afegeix almenys un compte corrent o d\'estalvi.',
|
||||||
|
'bs_onb_sal_link' => 'Sous',
|
||||||
|
'bs_onb_sal_desc' => 'Introdueix els ingressos de l\'any en curs.',
|
||||||
|
'bs_onb_items_link' => 'Línies de Pressupost',
|
||||||
|
'bs_onb_items_desc' => 'Ves a la pestanya Resum per llistar les teves despeses fixes (Lloguer, Assegurances...).',
|
||||||
|
'bs_rule_search_ph' => '🔍 Cercar una paraula clau (ex: AUCHAN)...',
|
||||||
|
'error_loading_settings' => 'Error en carregar la configuració',
|
||||||
|
'bud_err_delete_system_cat' => 'Impossible de suprimir una categoria de sistema essencial.',
|
||||||
|
|
||||||
|
// Budget : Suivi
|
||||||
'bud_rem_school' => 'Escola (Resta estimada)',
|
'bud_rem_school' => 'Escola (Resta estimada)',
|
||||||
'bud_rem_fmcg' => 'Compres (Resta estimada)',
|
'bud_rem_fmcg' => 'Compres (Resta estimada)',
|
||||||
'bud_rem_fuel' => 'Benzina (Resta estimada)',
|
'bud_rem_fuel' => 'Benzina (Resta estimada)',
|
||||||
@@ -411,16 +672,16 @@ return [
|
|||||||
'bud_import_csv' => 'Importar CSV',
|
'bud_import_csv' => 'Importar CSV',
|
||||||
'bud_csv_file' => 'Fitxer CSV (Punt i coma)',
|
'bud_csv_file' => 'Fitxer CSV (Punt i coma)',
|
||||||
'bud_preview' => 'Previsualitzar',
|
'bud_preview' => 'Previsualitzar',
|
||||||
'bud_validate_import' => 'Validar importació',
|
'bud_validate_import' => 'Si us plau, comproveu i categoritzeu les línies abans de validar.',
|
||||||
'bud_assign_month' => 'Assignar al mes',
|
'bud_assign_month' => 'Mes de gestió',
|
||||||
'bud_op_date' => 'Data Operació',
|
'bud_op_date' => 'Data',
|
||||||
'bud_label' => 'Concepte',
|
'bud_label' => 'Concepte',
|
||||||
'bud_amount' => 'Import',
|
'bud_amount' => 'Import',
|
||||||
'bud_category' => 'Categoria',
|
'bud_category' => 'Categoria',
|
||||||
'bud_ignore' => 'Ignorar',
|
'bud_ignore' => 'Ignorar (Crèdit)',
|
||||||
'bud_to_define' => 'Per definir',
|
'bud_to_define' => 'Per definir',
|
||||||
'bud_is_charge' => 'Despesa?',
|
'bud_is_charge' => 'És un càrrec fix...',
|
||||||
'bud_is_income' => 'Ingrés?',
|
'bud_is_income' => 'És un ingrés...',
|
||||||
'btn_import' => 'Importar',
|
'btn_import' => 'Importar',
|
||||||
'bud_no_lines' => 'Cap línia.',
|
'bud_no_lines' => 'Cap línia.',
|
||||||
'bud_confirm_delete' => 'Suprimir aquesta línia?',
|
'bud_confirm_delete' => 'Suprimir aquesta línia?',
|
||||||
@@ -439,23 +700,12 @@ return [
|
|||||||
'bud_add_title' => 'Afegir',
|
'bud_add_title' => 'Afegir',
|
||||||
'bud_edit_title' => 'Modificar la transacció',
|
'bud_edit_title' => 'Modificar la transacció',
|
||||||
'bud_to_define_js' => 'per definir',
|
'bud_to_define_js' => 'per definir',
|
||||||
'bud_bar_actual' => "Actual",
|
'bud_bar_actual' => 'Actual',
|
||||||
'bud_bar_theoretical' => "Teòric",
|
'bud_bar_theoretical' => 'Teòric',
|
||||||
'bud_bar_max_cap' => "Capacitat Màx.",
|
'bud_bar_max_cap' => 'Capacitat Màx.',
|
||||||
// --- MODULE BUDGET : IMPORT CSV ---
|
'bud_select_beneficiary' => 'Triar un ingrés esperat',
|
||||||
'bud_import_csv' => "Importar CSV",
|
|
||||||
'bud_csv_file' => "Seleccioneu el fitxer del vostre banc (.csv)",
|
|
||||||
'bud_preview' => "Previsualitzar",
|
|
||||||
'bud_validate_import' => "Si us plau, comproveu i categoritzeu les línies abans de validar.",
|
|
||||||
'bud_assign_month' => "Mes de gestió",
|
|
||||||
'bud_op_date' => "Data",
|
|
||||||
'bud_amount' => "Import",
|
|
||||||
'bud_ignore' => "Ignorar (Crèdit)",
|
|
||||||
'bud_to_define' => "Per definir",
|
|
||||||
'bud_is_charge' => "És un càrrec fix...",
|
|
||||||
'bud_is_income' => "És un ingrés...",
|
|
||||||
|
|
||||||
// --- BUDGET : RECAP ---
|
// Budget : Recap
|
||||||
'bud_recap_monthly_title' => 'Resum Mensual',
|
'bud_recap_monthly_title' => 'Resum Mensual',
|
||||||
'bud_recap_btn_add' => 'Afegir una despesa / ingrés',
|
'bud_recap_btn_add' => 'Afegir una despesa / ingrés',
|
||||||
'bud_planned_amount' => 'Import Previst',
|
'bud_planned_amount' => 'Import Previst',
|
||||||
@@ -495,42 +745,41 @@ return [
|
|||||||
'bud_label_regularization' => 'Regularització',
|
'bud_label_regularization' => 'Regularització',
|
||||||
'bud_reg_none' => 'Cap',
|
'bud_reg_none' => 'Cap',
|
||||||
'bud_recap_confirm_delete' => 'Segur que vols suprimir aquest element?',
|
'bud_recap_confirm_delete' => 'Segur que vols suprimir aquest element?',
|
||||||
'bud_err_delete' => 'Error en suprimir.',
|
'bud_prev_monthly_target' => 'Import objectiu / Mensualitat (€)',
|
||||||
'bud_sav_modal_title_add' => 'Introduir un mes',
|
'bud_prev_transfer_dest' => 'Destinació de la transferència',
|
||||||
'bud_sav_ph_name' => 'Nom (ex: Vacances)',
|
'bud_prev_none' => 'Cap destinació (Despesa estàndard)',
|
||||||
'bud_prev_label_name' => 'Nom',
|
|
||||||
|
|
||||||
// --- BUDGET : EPARGNE ---
|
// Budget : Epargne
|
||||||
'bud_sav_add_month' => 'Introduir un mes',
|
'bud_sav_add_month' => 'Introduir un mes',
|
||||||
'bud_sav_add_one_month' => '+1 Mes',
|
'bud_sav_add_one_month' => '+1 Mes',
|
||||||
'bud_sav_no_data' => 'Cap dada per a %s.',
|
'bud_sav_no_data' => 'Cap dada per a %s.',
|
||||||
'bud_sav_post_month' => 'Concepte / Mes',
|
'bud_sav_post_month' => 'Concepte / Mes',
|
||||||
'bud_sav_from_date' => 'Des del %s',
|
'bud_sav_from_date' => 'Des del %s',
|
||||||
'bud_sav_edit_modal' => 'Modificar amb Modal',
|
'bud_sav_edit_modal' => 'Editar aquest mes',
|
||||||
'bud_sav_delete_month' => 'Suprimir tot el mes',
|
'bud_sav_delete_month' => 'Suprimir aquest mes',
|
||||||
'bud_sav_total_bank' => 'Total Banc',
|
'bud_sav_total_bank' => 'TOTAL BANC',
|
||||||
'bud_sav_extra' => 'Extra',
|
'bud_sav_extra' => 'NO ASSIGNAT (EXTRES)',
|
||||||
'bud_sav_modal_title_add' => 'Introduir un mes',
|
'bud_sav_modal_title_add' => 'Afegir un mes',
|
||||||
'bud_sav_modal_title_edit' => 'Modificar:',
|
'bud_sav_modal_title_edit' => 'Editar',
|
||||||
'bud_sav_month_concerned' => 'Mes afectat',
|
'bud_sav_month_concerned' => 'Mes afectat',
|
||||||
'bud_sav_total_bank_eur' => 'Total al Banc (€)',
|
'bud_sav_total_bank_eur' => 'Total al Banc (€)',
|
||||||
'bud_sav_ventilation' => 'Desglossament',
|
'bud_sav_ventilation' => 'Desglossament de comptes',
|
||||||
'bud_sav_adj_help' => 'Utilitzeu l\'ajust (+/-) per recalcular automàticament.',
|
'bud_sav_adj_help' => 'Utilitzeu l\'ajust (+/-) per recalcular automàticament.',
|
||||||
'bud_sav_add_line' => 'Línia',
|
'bud_sav_add_line' => 'Afegir una línia',
|
||||||
'bud_sav_current' => 'Actual',
|
'bud_sav_current' => 'Actual',
|
||||||
'bud_sav_adjust' => 'Ajust (+/-)',
|
'bud_sav_adjust' => 'Ajust',
|
||||||
'bud_sav_new' => 'Nou',
|
'bud_sav_new' => 'Nou',
|
||||||
'bud_sav_sum_mode_title' => 'Activar la suma ràpida',
|
'bud_sav_sum_mode_title' => 'Activar/Desactivar la calculadora',
|
||||||
'bud_sav_selection' => 'Selecció:',
|
'bud_sav_selection' => 'Selecció:',
|
||||||
'bud_sav_ph_name' => 'Nom (ex: Vacances)',
|
'bud_sav_ph_name' => 'Nom del compte',
|
||||||
'bud_sav_saving' => 'Desant...',
|
'bud_sav_new_line' => 'Nova línia',
|
||||||
'bud_err_tech' => 'S\'ha produït un error tècnic.',
|
'bud_sav_prompt_new_line' => 'Nom de la nova línia d\'estalvi:',
|
||||||
'bud_sav_confirm_delete_month' => 'Suprimir TOT el mes de %m per a %o?',
|
'bud_sav_confirm_delete_month' => 'Estàs segur que vols suprimir totes les dades de %m per a %o?',
|
||||||
'bud_sav_prompt_duplicate' => "Vols copiar les dades de %s a %t1?\n\nIntrodueix el nou TOTAL al banc (€) per a %t2:",
|
'bud_sav_prompt_duplicate' => "Vols duplicar les dades de %s cap a %t1?\n\nIntrodueix el nou TOTAL al banc (€) per a %t2:",
|
||||||
'bud_err_server' => 'Error del servidor: ',
|
|
||||||
'bud_err_network_dup' => 'Error de xarxa en duplicar.',
|
'bud_err_network_dup' => 'Error de xarxa en duplicar.',
|
||||||
|
'bud_sav_saving' => 'Desant...',
|
||||||
|
|
||||||
// --- BUDGET : PREVISIONNEL ---
|
// Budget : Previsionnel
|
||||||
'bud_prev_incomes' => 'Ingressos',
|
'bud_prev_incomes' => 'Ingressos',
|
||||||
'bud_prev_person' => 'Persona',
|
'bud_prev_person' => 'Persona',
|
||||||
'bud_prev_salary' => 'Sou',
|
'bud_prev_salary' => 'Sou',
|
||||||
@@ -564,53 +813,94 @@ return [
|
|||||||
'bud_prev_err_no_history' => 'Impossible: no hi ha historial disponible per copiar.',
|
'bud_prev_err_no_history' => 'Impossible: no hi ha historial disponible per copiar.',
|
||||||
'bud_prev_confirm_copy' => "Vols copiar les dades de %s a %t?\n\n⚠️ Això sobreescriurà tots els valors ja presents per a %t.",
|
'bud_prev_confirm_copy' => "Vols copiar les dades de %s a %t?\n\n⚠️ Això sobreescriurà tots els valors ja presents per a %t.",
|
||||||
'bud_prev_confirm_transfers' => "Confirmes que %p ha fet totes les transferències per a %m?\n\nAixò actualitzarà l'Estalvi automàticament.",
|
'bud_prev_confirm_transfers' => "Confirmes que %p ha fet totes les transferències per a %m?\n\nAixò actualitzarà l'Estalvi automàticament.",
|
||||||
|
'bud_prev_label_name' => 'Nom',
|
||||||
|
'bud_prev_target_ph' => 'Ex: 150',
|
||||||
|
'bud_prev_transfer_to' => 'Cap a',
|
||||||
|
|
||||||
|
// Budget : Tricount / Avances
|
||||||
|
'bud_adv_title' => 'Avançaments i Tricount Familiar',
|
||||||
|
'bud_adv_add' => 'Afegir un avançament',
|
||||||
|
'bud_adv_edit' => 'Modificar l\'avançament',
|
||||||
|
'bud_adv_who_paid' => 'Qui ha pagat?',
|
||||||
|
'bud_adv_payer' => 'Pagador',
|
||||||
|
'bud_adv_has_advanced' => '%s ha avançat:',
|
||||||
|
'bud_adv_cc_label' => 'al compte comú',
|
||||||
|
'bud_adv_livret_label' => 'des del seu llibre d\'estalvis',
|
||||||
|
'bud_adv_cc_balance_title' => 'Equilibri del Compte Comú',
|
||||||
|
'bud_adv_owed_to' => 'S\'han de pagar <strong>%s €</strong> a <strong>%s</strong>',
|
||||||
|
'bud_adv_balanced' => 'Els comptes estan perfectament equilibrats!',
|
||||||
|
'bud_adv_saved_badge' => 'Estalvis',
|
||||||
|
'bud_adv_already_saved' => 'Aquests diners provenen d\'un llibre d\'estalvis',
|
||||||
|
'bud_adv_ph_desc' => 'Ex: Compres, Factura d\'electricitat...',
|
||||||
|
'bud_adv_confirm_resolve' => 'Confirmar el reemborsament complet d\'aquest avançament?',
|
||||||
|
'bud_adv_confirm_delete' => 'Eliminar definitivament aquest avançament?',
|
||||||
|
|
||||||
|
// Budget : Provisions & Optimisation
|
||||||
|
'budget_provisions_title' => 'Provisions & Optimisation de Trésorerie',
|
||||||
|
'btn_optimize_cashflow' => 'Optimiser mes Économies',
|
||||||
|
'add_new_provision' => 'Anticiper une grosse dépense à venir',
|
||||||
|
'provision_label' => 'Libellé de la dépense',
|
||||||
|
'provision_placeholder_wood' => 'Ex: Commande bois de chauffage, Révision voiture...',
|
||||||
|
'amount' => 'Montant',
|
||||||
|
'expected_date' => 'Date prévue',
|
||||||
|
'upcoming_provisions_list' => 'Dépenses prévues au calendrier',
|
||||||
|
'optimization_assistant_title' => 'Assistant d\'Optimisation des Quinzaines',
|
||||||
|
'success_add_provision' => 'Dépense prévisionnelle ajoutée !',
|
||||||
|
'no_provisions' => 'Aucune dépense prévisionnelle enregistrée pour le moment.',
|
||||||
|
'savings_this_month' => 'Économies globales à placer ce mois-ci',
|
||||||
|
'calculate' => 'Calculer la stratégie optimale',
|
||||||
|
'date_past_warning' => 'La date sélectionnée est déjà passée !',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// CADEAUX (GIFTS)
|
// 8. CADEAUX (GIFTS)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'gift_page_title' => 'HouseHub - Llista de regals',
|
'gift_page_title' => 'Llista de Regals',
|
||||||
'gift_occ_tio' => 'Tió',
|
'gift_main_title' => 'Regals de %s',
|
||||||
'gift_occ_noel' => 'Nadal',
|
'gift_filter_all_children' => 'Tots els nens',
|
||||||
'gift_occ_rois' => 'Reis',
|
'gift_filter_all_adults' => 'Tots els adults',
|
||||||
'gift_occ_anniv' => 'Aniversari',
|
'gift_add_gift' => 'Afegir',
|
||||||
'gift_occ_sant' => 'Sant',
|
'gift_empty_state_no_gifts' => 'Cap regal registrat per a aquesta ocasió.',
|
||||||
'gift_main_title' => 'Llista de regals %s',
|
'gift_empty_state_no_filter' => 'Cap regal coincideix amb els teus filtres.',
|
||||||
'gift_aria_change_view' => 'Canvia la vista',
|
'gift_paid_by' => 'Pagat per %s',
|
||||||
'gift_view_nadal' => 'Nadal',
|
'gift_liquidations' => 'Balanç i Reemborsaments',
|
||||||
'gift_view_anniv' => 'Aniversaris',
|
'gift_no_debt' => 'Tot està al dia, cap deute!',
|
||||||
'gift_view_by_party' => 'Vista per festa',
|
'gift_owes' => 'deu',
|
||||||
'gift_no_gifts' => 'No hi ha cap regal registrat per a %s en aquesta vista.',
|
'gift_view_matrix' => 'Veure la matriu detallada',
|
||||||
'gift_add_gift' => 'Afegeix un regal',
|
'gift_debtor' => 'Deutor',
|
||||||
'gift_paid_by' => 'pagat per %s',
|
'gift_creditor' => 'Creditor',
|
||||||
'gift_summary_title' => 'Resum del pressupost',
|
'gift_col_adult' => 'Per compte de',
|
||||||
'gift_col_adult' => 'Adult',
|
|
||||||
'gift_col_child' => 'Infant',
|
|
||||||
'gift_col_party' => 'Festa',
|
|
||||||
'gift_debtor' => 'Deutor',
|
|
||||||
'gift_creditor' => 'Creditor',
|
|
||||||
'gift_liquidations' => 'Tricount',
|
|
||||||
'gift_no_debt' => 'Cap deute pendent.',
|
|
||||||
'gift_owes' => 'ha de pagar',
|
|
||||||
'gift_to' => 'a',
|
|
||||||
'gift_detailed_list' => 'Llista detallada de regals',
|
|
||||||
'gift_col_gift' => 'Regal',
|
|
||||||
'gift_col_link' => 'Enllaç',
|
|
||||||
'gift_modal_title_add' => 'Afegeix un regal per a %s',
|
|
||||||
'gift_modal_title_edit' => 'Edita el regal',
|
|
||||||
'gift_modal_payer' => 'Pagat per',
|
'gift_modal_payer' => 'Pagat per',
|
||||||
'gift_modal_gift_name' => 'Nom del regal',
|
'gift_modal_gift_name' => 'Descripció del regal',
|
||||||
'gift_modal_ph_name' => 'p. ex., Lego Star Wars',
|
'gift_modal_price' => 'Preu estimat / pagat (€)',
|
||||||
'gift_modal_price' => 'Preu (€)',
|
'gift_modal_link' => 'Enllaç del producte (URL)',
|
||||||
'gift_modal_link' => 'Enllaç (opcional)',
|
'gift_modal_title_add' => 'Afegir per a %s',
|
||||||
'gift_confirm_delete' => 'Vols eliminar aquest regal?',
|
'gift_modal_title_edit' => 'Modificar el regal',
|
||||||
'gift_filter_all_children' => 'Tots els nens',
|
'gift_confirm_delete' => 'Vols eliminar realment aquest regal?',
|
||||||
'gift_filter_all_adults' => 'Tots els adults',
|
'gift_settings_title' => 'Configuració de les Festes',
|
||||||
'gift_empty_state_no_gifts' => 'Cap regal de moment.',
|
'gift_settings_add_title' => '+ Afegir una ocasió',
|
||||||
'gift_empty_state_no_filter' => 'Cap regal correspon al filtre.',
|
'gift_settings_name_placeholder' => 'Nom (ex: Sant Valentí)',
|
||||||
|
'gift_settings_date_placeholder' => 'MM-DD',
|
||||||
|
'gift_settings_date_help' => 'La data (MM-DD) és opcional. Deixa en blanc per a una festa mòbil.',
|
||||||
|
'gift_settings_empty' => 'Cap festa configurada.',
|
||||||
|
'gift_settings_active' => 'Actiu',
|
||||||
|
'gift_settings' => 'Configuració',
|
||||||
|
'gift_tab_occasions' => 'Festes i Ocasions',
|
||||||
|
'gift_tab_proches' => 'Familiars (Fora de la llar)',
|
||||||
|
'gift_settings_icon' => 'Icona',
|
||||||
|
'gift_settings_date' => 'Data (MM-DD)',
|
||||||
|
'gift_settings_add_occ' => 'Afegeix una festa',
|
||||||
|
'gift_settings_add_proche'=> 'Afegeix un familiar',
|
||||||
|
'gift_role_proche_adulte'=> 'Adult (Oncle, amic...)',
|
||||||
|
'gift_role_proche_enfant'=> 'Nen (Nebot, neboda...)',
|
||||||
|
'gift_ph_proche_name' => 'Nom',
|
||||||
|
'gift_role_label' => 'Rol',
|
||||||
|
'gift_settings_saved' => 'Configuració desada!',
|
||||||
|
'gift_settings_edit_occ' => 'Modificar la festa',
|
||||||
|
'gift_settings_edit_proche' => 'Modificar el familiar',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// GARAGE MANAGER
|
// 9. GARAGE
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'menu_garage' => 'Garatge',
|
|
||||||
'garage_page_title' => 'HouseHub - Garatge',
|
'garage_page_title' => 'HouseHub - Garatge',
|
||||||
'garage_stat_vehicles' => 'Vehicles',
|
'garage_stat_vehicles' => 'Vehicles',
|
||||||
'garage_stat_maintenances' => 'Manteniments',
|
'garage_stat_maintenances' => 'Manteniments',
|
||||||
@@ -619,7 +909,7 @@ return [
|
|||||||
'garage_my_vehicles' => 'Els meus vehicles',
|
'garage_my_vehicles' => 'Els meus vehicles',
|
||||||
'garage_reminders' => 'Recordatoris i propers manteniments',
|
'garage_reminders' => 'Recordatoris i propers manteniments',
|
||||||
'garage_vehicles_title' => 'Els meus vehicles',
|
'garage_vehicles_title' => 'Els meus vehicles',
|
||||||
'garage_all_parts' => 'Totes les peces',
|
'garage_all_parts' => 'Toutes les peces',
|
||||||
'garage_parts_count' => 'peces',
|
'garage_parts_count' => 'peces',
|
||||||
'garage_maintenances' => 'Manteniments',
|
'garage_maintenances' => 'Manteniments',
|
||||||
'garage_parts' => 'Peces',
|
'garage_parts' => 'Peces',
|
||||||
@@ -640,7 +930,6 @@ return [
|
|||||||
'garage_photo' => 'Foto',
|
'garage_photo' => 'Foto',
|
||||||
'garage_notes' => 'Notes',
|
'garage_notes' => 'Notes',
|
||||||
'garage_maint_type' => 'Tipus de manteniment',
|
'garage_maint_type' => 'Tipus de manteniment',
|
||||||
'garage_date' => 'Data',
|
|
||||||
'garage_km_at' => 'Quilometratge en el moment',
|
'garage_km_at' => 'Quilometratge en el moment',
|
||||||
'garage_labor_cost' => 'Cost mà d\'obra (€)',
|
'garage_labor_cost' => 'Cost mà d\'obra (€)',
|
||||||
'garage_description' => 'Descripció',
|
'garage_description' => 'Descripció',
|
||||||
@@ -659,4 +948,79 @@ return [
|
|||||||
'garage_change_photo' => 'Canviar la foto',
|
'garage_change_photo' => 'Canviar la foto',
|
||||||
'garage_new_photo' => 'Nova foto',
|
'garage_new_photo' => 'Nova foto',
|
||||||
'garage_update_photo' => 'Actualitzar',
|
'garage_update_photo' => 'Actualitzar',
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 10. LISTES & COURSES
|
||||||
|
// ==========================================
|
||||||
|
'liste_placeholder_add' => 'Afegir un article...',
|
||||||
|
'liste_btn_add' => 'Afegir',
|
||||||
|
'liste_btn_uncheck_all' => 'Tot desmarcar',
|
||||||
|
'liste_btn_delete_picked' => 'Suprimir els marcats',
|
||||||
|
'liste_btn_clear_all' => 'Buidar la llista',
|
||||||
|
'liste_subtitle' => '{pending} pendents · {cart} marcats',
|
||||||
|
'liste_added' => 'Afegit',
|
||||||
|
'liste_updated' => 'Modificat',
|
||||||
|
'liste_deleted' => 'Suprimit',
|
||||||
|
'liste_already_in' => 'Ja a la llista',
|
||||||
|
'liste_confirm_clear' => 'Buidar tota la llista?',
|
||||||
|
'liste_confirm_delete_list' => 'Suprimir aquesta llista i tots els articles?',
|
||||||
|
'liste_history_title' => 'Recents — clic per afegir',
|
||||||
|
'liste_new_list' => 'Nova llista',
|
||||||
|
'liste_list_name_placeholder'=> 'Nom de la llista...',
|
||||||
|
'liste_create' => 'Crear',
|
||||||
|
'liste_rename_list' => 'Renombrar',
|
||||||
|
'liste_new_name' => 'Nou nom:',
|
||||||
|
'liste_list_deleted' => 'Llista suprimida',
|
||||||
|
'liste_list_created' => 'Llista creada',
|
||||||
|
'liste_list_renamed' => 'Llista renombrada',
|
||||||
|
'liste_edit_placeholder' => 'Modificar l\'article:',
|
||||||
|
'liste_empty_state' => 'Creeu la primera llista amb el botó + de dalt.',
|
||||||
|
'liste_settings_title' => 'Llistes — historial',
|
||||||
|
'liste_settings_intro' => 'Nombre màxim d\'articles memoritzats a l\'historial (comú per a totes).',
|
||||||
|
'liste_settings_history_max' => 'Mida de l\'historial',
|
||||||
|
'liste_settings_history_hint'=> 'Entre 1 i 50.',
|
||||||
|
'groceries_settings_saved' => 'Preferències desades.',
|
||||||
|
'groceries_settings_error' => 'Impossible de desar:',
|
||||||
|
'groceries_err_duplicate' => 'Aquest producte ja és a la llista.',
|
||||||
|
|
||||||
|
// --- MODULE MEALS ---
|
||||||
|
'menu_meals' => 'Menús',
|
||||||
|
'mod_meals_name' => 'Planificació de Menús',
|
||||||
|
'mod_meals_desc' => 'Planifica els àpats de la setmana per a tota la família.',
|
||||||
|
'meals_board_title' => 'Planificació de Menús',
|
||||||
|
'meals_copy_lunch' => 'Copiar tot el dinar',
|
||||||
|
'meals_copy_dinner' => 'Copiar tot el sopar',
|
||||||
|
'meal_lunch' => 'Migdia',
|
||||||
|
'meal_dinner' => 'Sopar',
|
||||||
|
'btn_apply_all' => 'Tots',
|
||||||
|
'btn_copy_to' => 'Copiar cap a...',
|
||||||
|
'modal_copy_title' => 'Copiar cap a...',
|
||||||
|
'source_individual' => 'Plat individual',
|
||||||
|
'source_row' => 'Àpat complet',
|
||||||
|
'week_current' => 'Setmana actual',
|
||||||
|
'week_next' => 'Setmana vinent',
|
||||||
|
'meals_no_members' => 'Si us plau, configureu els membres de la vostra família a la configuració.',
|
||||||
|
'day_full_mon' => 'Dilluns', 'day_full_tue' => 'Dimarts', 'day_full_wed' => 'Dimecres',
|
||||||
|
'day_full_thu' => 'Dijous', 'day_full_fri' => 'Divendres', 'day_full_sat' => 'Dissabte', 'day_full_sun' => 'Diumenge',
|
||||||
|
'meal_title' => 'Planificador de Menjars',
|
||||||
|
'meal_edit_title' => 'Modificar el menjar',
|
||||||
|
'meal_label_name' => 'Menú previst',
|
||||||
|
'meal_week' => 'Setmana',
|
||||||
|
'meal_ph_name' => 'Ex: Lasanya casolana',
|
||||||
|
'meal_empty_slot' => 'Per definir...',
|
||||||
|
'meal_current_week' => 'Setmana actual',
|
||||||
|
'meal_saved' => 'Menjar guardat!',
|
||||||
|
'meal_btn_import_pdf' => 'Import Menu PDF',
|
||||||
|
'meal_import_title' => 'Import canteen menu',
|
||||||
|
'meal_import_child' => 'For which child?',
|
||||||
|
'meal_select_child' => 'Select child',
|
||||||
|
'meal_pdf_file' => 'PDF File (Convivio)',
|
||||||
|
'meal_drop_pdf' => 'Click to add PDF',
|
||||||
|
'meal_import_success' => 'Menus imported successfully!',
|
||||||
|
'mod_food_name' => 'Cuina i Compres',
|
||||||
|
'food_tab_meals' => 'Menú de la setmana',
|
||||||
|
'food_tab_list' => 'Llista de la compra',
|
||||||
|
'menu_food' => 'Menjar',
|
||||||
|
'mod_food_desc' => 'Gestioneu els vostres menús setmanals i les llistes de la compra compartides.',
|
||||||
|
'cta_food' => 'Cuinar',
|
||||||
];
|
];
|
||||||
+533
-152
@@ -1,29 +1,23 @@
|
|||||||
<?php
|
<?php
|
||||||
// English Dictionary
|
// English Dictionary - HouseHub
|
||||||
return [
|
return [
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// TEST AUTO
|
// 1. GLOBAL, NAVIGATION & ACTIONS
|
||||||
// ==========================================
|
|
||||||
'test_running' => 'Running tests...',
|
|
||||||
'test_passed' => 'Test passed',
|
|
||||||
'test_failed' => 'Test failed',
|
|
||||||
'tests_title' => 'Test Lab',
|
|
||||||
'tests_run_budget' => 'Test Budget',
|
|
||||||
'tests_run_all' => 'Run all tests',
|
|
||||||
'tests_copy_report' => 'Copy report',
|
|
||||||
'tests_waiting' => 'Waiting to start...',
|
|
||||||
'tests_report_copied' => 'Report copied to clipboard!',
|
|
||||||
'tests_live_preview' => 'Live preview',
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// GLOBAL & NAVIGATION
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'menu_home' => 'Home',
|
'menu_home' => 'Home',
|
||||||
'menu_calendar' => 'Calendar',
|
'menu_calendar' => 'Calendar',
|
||||||
'menu_budget' => 'Budget',
|
'menu_budget' => 'Budget',
|
||||||
'menu_holidays' => 'Trips',
|
'menu_holidays' => 'Trips',
|
||||||
'menu_gifts' => 'Gifts',
|
'menu_gifts' => 'Gifts',
|
||||||
|
'menu_garage' => 'Garage',
|
||||||
|
'menu_memo' => 'Notes',
|
||||||
|
'menu_todo' => 'Todo',
|
||||||
|
'menu_liste' => 'Lists',
|
||||||
|
'menu_groceries' => 'Groceries',
|
||||||
'menu_calendar_ios' => 'iOS Calendar',
|
'menu_calendar_ios' => 'iOS Calendar',
|
||||||
|
'menu_printvault' => 'PrintVault',
|
||||||
|
'menu_planka' => 'Planka',
|
||||||
|
|
||||||
'nav_home' => 'Home',
|
'nav_home' => 'Home',
|
||||||
'nav_calendar' => 'Calendar',
|
'nav_calendar' => 'Calendar',
|
||||||
'nav_budget' => 'Budget',
|
'nav_budget' => 'Budget',
|
||||||
@@ -31,7 +25,6 @@ return [
|
|||||||
'nav_gifts' => 'Gifts',
|
'nav_gifts' => 'Gifts',
|
||||||
'aria_menu' => 'Mobile menu',
|
'aria_menu' => 'Mobile menu',
|
||||||
|
|
||||||
// Buttons & global actions
|
|
||||||
'actions' => 'Actions',
|
'actions' => 'Actions',
|
||||||
'edit' => 'Edit',
|
'edit' => 'Edit',
|
||||||
'delete' => 'Delete',
|
'delete' => 'Delete',
|
||||||
@@ -41,28 +34,33 @@ return [
|
|||||||
'btn_cancel' => 'Cancel',
|
'btn_cancel' => 'Cancel',
|
||||||
'btn_delete' => 'Delete',
|
'btn_delete' => 'Delete',
|
||||||
'btn_close' => 'Close',
|
'btn_close' => 'Close',
|
||||||
|
'btn_add' => 'Add',
|
||||||
'btn_back' => '◀ Back',
|
'btn_back' => '◀ Back',
|
||||||
'btn_edit_bases' => '⚙️ Edit base values',
|
'btn_edit_bases' => '⚙️ Edit base values',
|
||||||
'btn_edit' => 'Edit',
|
'btn_edit' => 'Edit',
|
||||||
'error_occured' => 'An error occurred.',
|
|
||||||
|
|
||||||
// Generic words & errors
|
|
||||||
'error' => 'Error.',
|
'error' => 'Error.',
|
||||||
|
'error_occured' => 'An error occurred.',
|
||||||
|
'error_network' => 'Network error',
|
||||||
|
'error_generic' => 'An error occurred',
|
||||||
'confirm_delete' => 'Are you sure you want to delete this item?',
|
'confirm_delete' => 'Are you sure you want to delete this item?',
|
||||||
|
'loading' => 'Loading...',
|
||||||
|
'date' => 'Date',
|
||||||
|
'settings' => 'Settings',
|
||||||
|
|
||||||
// Months & Days
|
// Months & Days
|
||||||
'month_01' => 'January', 'month_02' => 'February', 'month_03' => 'March',
|
'month_01' => 'January', 'month_02' => 'February', 'month_03' => 'March', 'month_04' => 'April', 'month_05' => 'May', 'month_06' => 'June',
|
||||||
'month_04' => 'April', 'month_05' => 'May', 'month_06' => 'June',
|
'month_07' => 'July', 'month_08' => 'August', 'month_09' => 'September', 'month_10' => 'October', 'month_11' => 'November', 'month_12' => 'December',
|
||||||
'month_07' => 'July', 'month_08' => 'August', 'month_09' => 'September',
|
'month_jan' => 'January', 'month_feb' => 'February', 'month_mar' => 'March', 'month_apr' => 'April', 'month_may' => 'May', 'month_jun' => 'June',
|
||||||
'month_10' => 'October', 'month_11' => 'November', 'month_12' => 'December',
|
'month_jul' => 'July', 'month_aug' => 'August', 'month_sep' => 'September', 'month_oct' => 'October', 'month_nov' => 'November', 'month_dec' => 'December',
|
||||||
'day_mon' => 'Mon', 'day_tue' => 'Tue', 'day_wed' => 'Wed',
|
'day_mon' => 'Mon', 'day_tue' => 'Tue', 'day_wed' => 'Wed', 'day_thu' => 'Thu', 'day_fri' => 'Fri', 'day_sat' => 'Sat', 'day_sun' => 'Sun',
|
||||||
'day_thu' => 'Thu', 'day_fri' => 'Fri', 'day_sat' => 'Sat', 'day_sun' => 'Sun',
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// AUTHENTICATION / LOGIN
|
// 2. AUTHENTICATION / LOGIN
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'login_title' => 'Login - HouseHub',
|
'login_title' => 'Login - HouseHub',
|
||||||
'login_header' => 'Login',
|
'login_header' => 'Login',
|
||||||
|
'login_subtitle' => 'Please sign in to continue',
|
||||||
'label_username' => 'Username',
|
'label_username' => 'Username',
|
||||||
'label_password' => 'Password',
|
'label_password' => 'Password',
|
||||||
'placeholder_username' => 'e.g. john',
|
'placeholder_username' => 'e.g. john',
|
||||||
@@ -70,18 +68,19 @@ return [
|
|||||||
'btn_login_submit' => 'Sign in',
|
'btn_login_submit' => 'Sign in',
|
||||||
'logout' => 'Logout',
|
'logout' => 'Logout',
|
||||||
'btn_logout' => 'Logout',
|
'btn_logout' => 'Logout',
|
||||||
|
'login_remember_me' => 'Remember me',
|
||||||
'error_missing_fields' => 'All fields are required.',
|
'error_missing_fields' => 'All fields are required.',
|
||||||
'error_invalid_credentials' => 'Invalid credentials.',
|
'error_invalid_credentials' => 'Invalid credentials.',
|
||||||
'login_subtitle' => 'Please sign in to continue',
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// HOME / INDEX
|
// 3. HOME & DASHBOARD
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'home_title' => 'HouseHub - Home',
|
'home_title' => 'HouseHub - Home',
|
||||||
'home_welcome' => 'Welcome to HouseHub',
|
'home_welcome' => 'Welcome to HouseHub',
|
||||||
'home_subtitle' => 'Family organisation control centre.',
|
'home_subtitle' => 'Family organisation control centre.',
|
||||||
'home_logged_as' => 'Logged in as',
|
'home_logged_as' => 'Logged in as',
|
||||||
'home_modules_title' => 'Modules',
|
'home_modules_title' => 'Modules',
|
||||||
|
|
||||||
'mod_calendar_name' => 'Family Calendar',
|
'mod_calendar_name' => 'Family Calendar',
|
||||||
'mod_calendar_desc' => 'Manage leave, childcare arrangements and weekly planning.',
|
'mod_calendar_desc' => 'Manage leave, childcare arrangements and weekly planning.',
|
||||||
'mod_holidays_name' => 'Trips',
|
'mod_holidays_name' => 'Trips',
|
||||||
@@ -94,52 +93,19 @@ return [
|
|||||||
'mod_garage_desc' => 'Track vehicle maintenance, parts used and costs.',
|
'mod_garage_desc' => 'Track vehicle maintenance, parts used and costs.',
|
||||||
'mod_memo_name' => 'Notes',
|
'mod_memo_name' => 'Notes',
|
||||||
'mod_memo_desc' => 'Your memos, recipes, instructions and miscellaneous info with Markdown and attachments.',
|
'mod_memo_desc' => 'Your memos, recipes, instructions and miscellaneous info with Markdown and attachments.',
|
||||||
'menu_memo' => 'Notes',
|
|
||||||
'mod_todo_name' => 'Todo',
|
'mod_todo_name' => 'Todo',
|
||||||
'mod_todo_desc' => 'Manage tasks, shopping lists and family reminders.',
|
'mod_todo_desc' => 'Manage tasks, shopping lists and family reminders.',
|
||||||
'mod_calendar_ios_name' => 'iOS Calendar',
|
'mod_liste_name' => 'Lists',
|
||||||
'mod_calendar_ios_desc' => 'Sync HouseHub events with your iPhone calendar (iCloud CalDAV).',
|
'mod_liste_desc' => 'Shared named lists — groceries, DIY, pharmacy… As many lists as needed.',
|
||||||
'menu_todo' => 'Todo',
|
|
||||||
'mod_groceries_name' => 'Groceries',
|
'mod_groceries_name' => 'Groceries',
|
||||||
'mod_groceries_desc' => 'Shared list: add items and check them off when they are in your cart.',
|
'mod_groceries_desc' => 'Shared list: add items and check them off when they are in your cart.',
|
||||||
'menu_groceries' => 'Groceries',
|
'mod_calendar_ios_name' => 'iOS Calendar',
|
||||||
'groceries_placeholder_add' => 'e.g. milk, bread, tomatoes…',
|
'mod_calendar_ios_desc' => 'Sync HouseHub events with your iPhone calendar (iCloud CalDAV).',
|
||||||
'groceries_btn_add' => 'Add',
|
'mod_printvault_name'=> 'PrintVault',
|
||||||
'groceries_btn_uncheck_all' => 'Reset cart checks',
|
'mod_printvault_desc'=> 'Manage your 3D print files — STL, 3MF, GCode with interactive viewer.',
|
||||||
'groceries_btn_delete_picked' => 'Remove checked items',
|
'mod_planka_name' => 'Planka',
|
||||||
'groceries_subtitle' => '{pending} to pick up · {cart} in cart',
|
'mod_planka_desc' => 'Manage your projects in Kanban mode with Planka.',
|
||||||
'groceries_added' => 'Added',
|
|
||||||
'groceries_updated' => 'Updated',
|
|
||||||
'groceries_deleted' => 'Removed',
|
|
||||||
'groceries_reset_next' => 'List ready for your next trip',
|
|
||||||
'groceries_picked_removed' => 'Checked items removed',
|
|
||||||
'groceries_label_empty' => 'Label cannot be empty',
|
|
||||||
'groceries_confirm_del_title' => 'Delete?',
|
|
||||||
'groceries_confirm_del_body' => 'Remove this product from the list?',
|
|
||||||
'groceries_confirm_uncheck_title' => 'Reset cart checks',
|
|
||||||
'groceries_confirm_uncheck_body' => 'All “in cart” checkmarks will be cleared. Items stay on the list.',
|
|
||||||
'groceries_confirm_clear_title' => 'Remove checked items',
|
|
||||||
'groceries_confirm_clear_body' => 'Every checked item will be deleted from the list.',
|
|
||||||
'groceries_empty' => 'Nothing yet. Add a product above.',
|
|
||||||
'groceries_aria_toggle' => 'Mark as in cart or still to pick up',
|
|
||||||
'groceries_section_pending' => 'To pick up',
|
|
||||||
'groceries_section_cart' => 'In cart',
|
|
||||||
'groceries_prompt_edit' => 'Edit product',
|
|
||||||
'groceries_btn_clear_list' => 'Clear list',
|
|
||||||
'groceries_confirm_empty_list_title' => 'Clear the entire list?',
|
|
||||||
'groceries_confirm_empty_list_body' => 'All products will be removed. They stay in history so you can add them again quickly.',
|
|
||||||
'groceries_list_cleared' => 'List cleared',
|
|
||||||
'groceries_history_title' => 'History — click to add',
|
|
||||||
'groceries_history_empty' => 'History fills as you add products (up to the limit set in Settings).',
|
|
||||||
'groceries_history_meta' => 'Up to {max} products · ',
|
|
||||||
'groceries_settings_link' => 'Settings',
|
|
||||||
'groceries_settings_title' => 'Groceries — history',
|
|
||||||
'groceries_settings_intro' => 'Maximum number of products remembered in history (no duplicates, case-insensitive).',
|
|
||||||
'groceries_settings_history_max' => 'History size',
|
|
||||||
'groceries_settings_history_hint' => 'Between 1 and 50. Oldest entries are removed when the limit is exceeded.',
|
|
||||||
'groceries_settings_saved' => 'Grocery preferences saved.',
|
|
||||||
'groceries_settings_error' => 'Could not save:',
|
|
||||||
'groceries_err_duplicate' => 'This product is already on the list.',
|
|
||||||
'cta_open' => 'Open',
|
'cta_open' => 'Open',
|
||||||
'cta_explore' => 'Explore',
|
'cta_explore' => 'Explore',
|
||||||
'cta_view_lists' => 'Unwrap',
|
'cta_view_lists' => 'Unwrap',
|
||||||
@@ -149,16 +115,123 @@ return [
|
|||||||
'cta_check' => 'Check off',
|
'cta_check' => 'Check off',
|
||||||
'cta_cart' => 'Go shopping',
|
'cta_cart' => 'Go shopping',
|
||||||
'cta_sync' => 'Sync',
|
'cta_sync' => 'Sync',
|
||||||
'mod_printvault_name' => 'PrintVault',
|
|
||||||
'mod_printvault_desc' => 'Manage your 3D print files — STL, 3MF, GCode with interactive viewer.',
|
|
||||||
'menu_printvault' => 'PrintVault',
|
|
||||||
'cta_print' => 'Print',
|
'cta_print' => 'Print',
|
||||||
'mod_planka_name' => 'Planka',
|
|
||||||
'mod_planka_desc' => 'Manage your projects in Kanban mode with Planka.',
|
|
||||||
'menu_planka' => 'Planka',
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// FAMILY CALENDAR
|
// 4. SETTINGS
|
||||||
|
// ==========================================
|
||||||
|
'set_title_page' => '⚙️ Settings',
|
||||||
|
'set_title_lang' => '🌐 Language',
|
||||||
|
'set_title_modules' => '🧩 Active Modules',
|
||||||
|
'set_desc_modules' => 'Choose the modules visible in the navigation.',
|
||||||
|
'set_title_ios' => '📱 iOS Calendar Integration (CalDAV)',
|
||||||
|
'set_desc_ios' => 'Configure your iCloud calendar here to sync events.',
|
||||||
|
'set_label_apple_id' => 'Apple ID (iCloud email)',
|
||||||
|
'set_label_apple_pwd' => 'Apple app password',
|
||||||
|
'set_label_apple_url' => 'CalDAV calendar URL',
|
||||||
|
'set_btn_test_ios' => 'Test connection',
|
||||||
|
'set_btn_disconnect_ios' => 'Disconnect',
|
||||||
|
'set_title_bg' => '🖼️ Home Image',
|
||||||
|
'set_desc_bg' => 'Background photo displayed on the homepage.',
|
||||||
|
'set_badge_custom_bg' => 'Custom image',
|
||||||
|
'set_label_new_bg' => 'New image (jpg, png, webp — max 10 MB)',
|
||||||
|
'set_btn_reset_bg' => '🔄 Reset to default image',
|
||||||
|
'set_confirm_reset_bg' => 'Revert to default image?',
|
||||||
|
'set_title_profile' => '👤 My Profile',
|
||||||
|
'set_label_username' => 'Username',
|
||||||
|
'set_label_display_name' => 'First name / Display name',
|
||||||
|
'set_label_profile_color' => 'My profile theme color',
|
||||||
|
'set_desc_profile_color' => 'This color will be used for your budget grids, calendars, and personal indicators.',
|
||||||
|
'set_title_pwd' => '🔒 Change Password',
|
||||||
|
'set_label_cur_pwd' => 'Current password',
|
||||||
|
'set_label_new_pwd' => 'New password',
|
||||||
|
'set_label_confirm_pwd' => 'Confirm',
|
||||||
|
'set_btn_edit' => 'Edit',
|
||||||
|
'set_title_family' => '🏠 My Family Space',
|
||||||
|
'set_label_family_name' => 'Space name',
|
||||||
|
'set_desc_invite' => 'Invitation code — share it to invite someone',
|
||||||
|
'set_title_copy' => 'Click to copy',
|
||||||
|
'set_msg_copied' => '✓ Copied!',
|
||||||
|
'set_btn_new_code' => '🔄 New code',
|
||||||
|
'set_confirm_new_code' => 'Regenerate code? The old one will be invalidated.',
|
||||||
|
'set_desc_register_link' => 'Registration link:',
|
||||||
|
'set_title_members' => 'Members',
|
||||||
|
'set_badge_admin' => 'Admin',
|
||||||
|
'set_title_attached' => '🧸 Attached Profiles',
|
||||||
|
'set_desc_attached' => 'Household members without direct access.',
|
||||||
|
'set_empty_attached' => 'No attached profiles.',
|
||||||
|
'set_link_admin' => '→ Administration Panel',
|
||||||
|
'set_kids_title' => 'Household Kids',
|
||||||
|
'set_kids_desc' => 'Manage the list of kids to associate them with savings, gifts, etc.',
|
||||||
|
'set_add_kid_placeholder' => 'Kid\'s first name',
|
||||||
|
'set_btn_add_kid' => 'Add',
|
||||||
|
'set_err_kid_name' => 'The first name is required.',
|
||||||
|
'set_confirm_del_kid' => 'Delete this kid? (Their financial history will remain visible in the archives).',
|
||||||
|
'set_no_kids' => 'No kids configured for this household.',
|
||||||
|
'set_family_config' => 'Family Structure',
|
||||||
|
'set_global_params' => 'Global settings',
|
||||||
|
'set_care_modes' => 'Common childcare arrangements',
|
||||||
|
'set_add_care_mode' => 'Add a mode',
|
||||||
|
|
||||||
|
// Labels Profiles
|
||||||
|
'label_firstname' => 'First Name',
|
||||||
|
'placeholder_firstname_ex' => 'e.g., Pol, Carole...',
|
||||||
|
'label_role' => 'Role',
|
||||||
|
'role_child' => '👶 Child',
|
||||||
|
'role_child_icon' => '👶 Child',
|
||||||
|
'role_helper' => '💼 Helper',
|
||||||
|
'role_helper_icon' => '💼 Helper',
|
||||||
|
'role_relative' => '👵 Relative',
|
||||||
|
'role_relative_icon' => '👵 Relative',
|
||||||
|
'role_unknown' => 'Unknown',
|
||||||
|
'label_color' => 'Color',
|
||||||
|
'change_color' => 'Change color',
|
||||||
|
'confirm_delete_profile' => 'Permanently delete this profile?',
|
||||||
|
|
||||||
|
// Settings Messages
|
||||||
|
'error_csrf' => 'Invalid session (CSRF). Please reload the page.',
|
||||||
|
'set_err_min_module' => 'You must keep at least one active module.',
|
||||||
|
'set_success_modules' => 'Modules successfully updated.',
|
||||||
|
'set_success_lang' => 'Language updated.',
|
||||||
|
'set_err_empty_name' => 'First name cannot be empty.',
|
||||||
|
'set_success_profile' => 'Profile updated successfully.',
|
||||||
|
'set_err_wrong_pwd' => 'Incorrect current password.',
|
||||||
|
'set_err_pwd_length' => 'New password must be at least 6 characters long.',
|
||||||
|
'set_err_pwd_match' => 'Passwords do not match.',
|
||||||
|
'set_success_pwd' => 'Password successfully changed.',
|
||||||
|
'set_success_family' => 'Space name updated.',
|
||||||
|
'set_success_invite' => 'New invitation code generated.',
|
||||||
|
'set_err_bg_format' => 'Unsupported format (jpg, png, webp).',
|
||||||
|
'set_success_bg' => 'Home image updated.',
|
||||||
|
'set_err_upload' => 'Error during upload.',
|
||||||
|
'set_success_bg_reset' => 'Home image reset.',
|
||||||
|
'set_err_db_unavailable' => 'Family database unavailable.',
|
||||||
|
'set_err_ios_missing' => 'Please provide an iCloud ID, app password, and CalDAV URL.',
|
||||||
|
'set_success_ios' => 'iOS calendar connection saved.',
|
||||||
|
'set_success_ios_auto' => ' Calendar URL automatically detected.',
|
||||||
|
'set_err_ios_save' => 'Cannot save iOS connection: ',
|
||||||
|
'set_err_ios_none' => 'No iOS connection configured.',
|
||||||
|
'set_success_ios_test' => 'Valid iCloud CalDAV connection (HTTP %s).',
|
||||||
|
'set_err_ios_test' => 'Connection test failed (HTTP %s). Check your credentials.',
|
||||||
|
'set_err_ios_test_fail' => 'Connection test unavailable: ',
|
||||||
|
'set_success_ios_del' => 'iOS connection removed.',
|
||||||
|
'set_success_profile_add' => 'Profile for %s successfully added.',
|
||||||
|
'set_err_name_required' => 'First name is required.',
|
||||||
|
'set_success_profile_del' => 'Profile deleted.',
|
||||||
|
'set_success_color' => 'Color updated!',
|
||||||
|
'set_success_color_profile' => 'Profile color updated.',
|
||||||
|
'set_success_color_update' => 'Color updated!',
|
||||||
|
'color_updated' => 'Color updated!',
|
||||||
|
'settings_profile_added' => 'Profile of %s added successfully.',
|
||||||
|
'settings_err_name_required' => 'First name is required.',
|
||||||
|
'settings_attached_profiles_title' => 'Attached Profiles',
|
||||||
|
'settings_attached_profiles_desc' => 'Household members without direct access.',
|
||||||
|
'settings_no_attached_profiles' => 'No attached profiles.',
|
||||||
|
'placeholder_apple_id' => 'name@icloud.com',
|
||||||
|
'placeholder_apple_pwd' => 'xxxx-xxxx-xxxx-xxxx',
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 5. FAMILY CALENDAR
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'fc_page_title' => 'HouseHub - Calendar',
|
'fc_page_title' => 'HouseHub - Calendar',
|
||||||
'fc_main_header' => 'Family Calendar',
|
'fc_main_header' => 'Family Calendar',
|
||||||
@@ -183,6 +256,7 @@ return [
|
|||||||
'fc_weekly_planning' => 'Weekly planning',
|
'fc_weekly_planning' => 'Weekly planning',
|
||||||
'fc_col_month' => 'Month',
|
'fc_col_month' => 'Month',
|
||||||
'fc_col_week_short' => 'Wk.',
|
'fc_col_week_short' => 'Wk.',
|
||||||
|
'fc_col_week' => 'Week',
|
||||||
'fc_col_av' => 'Av.',
|
'fc_col_av' => 'Av.',
|
||||||
'fc_col_use' => 'Use',
|
'fc_col_use' => 'Use',
|
||||||
'fc_legend_title' => 'Legend',
|
'fc_legend_title' => 'Legend',
|
||||||
@@ -193,14 +267,98 @@ return [
|
|||||||
'fc_unit_days' => 'days',
|
'fc_unit_days' => 'days',
|
||||||
'fc_menu_carole' => 'Carole\'s Leave',
|
'fc_menu_carole' => 'Carole\'s Leave',
|
||||||
'fc_clear' => 'Clear',
|
'fc_clear' => 'Clear',
|
||||||
'fc_menu_kids_leaves' => 'Children\'s Leave',
|
'fc_menu_kids_leaves' => 'Parent\'s Leave',
|
||||||
'fc_err_save_snap' => 'Error saving adjustment.',
|
'fc_err_save_snap' => 'Error saving adjustment.',
|
||||||
'fc_err_fetch_gov' => 'Error connecting to the government API.',
|
'fc_err_fetch_gov' => 'Error connecting to the government API.',
|
||||||
'fc_err_no_data_gov' => 'No data found on the government API for this year.',
|
'fc_err_no_data_gov' => 'No data found on the government API for this year.',
|
||||||
'fc_err_action' => 'Error during action: ',
|
'fc_err_action' => 'Error during action: ',
|
||||||
|
'fc_err_select_leave' => 'Please select a leave type.',
|
||||||
'fc_today_short' => 'Today',
|
'fc_today_short' => 'Today',
|
||||||
'fc_today_title' => 'Today',
|
'fc_today_title' => 'Today',
|
||||||
|
'fc_tab_foyer' => '🏡 Household',
|
||||||
|
'fc_tab_members' => '👥 Members & Rights',
|
||||||
|
'fc_select_member' => 'Select a family member:',
|
||||||
|
'fc_role_adult' => 'Adult',
|
||||||
|
'fc_role_child' => 'Child',
|
||||||
|
'fc_role_helper' => 'Helper',
|
||||||
|
'fc_role_unknown' => 'Unknown',
|
||||||
|
'fc_care_modes_title' => 'Associated Care Modes',
|
||||||
|
'fc_no_care_mode' => 'No care mode configured.',
|
||||||
|
'fc_matrix_title' => '👨👩👦 Leave Rights Matrix',
|
||||||
|
'fc_matrix_desc' => 'Declare your counter codes and renewal dates.',
|
||||||
|
'fc_matrix_code' => 'Code',
|
||||||
|
'fc_matrix_date' => 'Reset Date',
|
||||||
|
'fc_matrix_empty' => 'No counter configured.',
|
||||||
|
'fc_matrix_saved' => 'Rights matrix updated successfully! 💼',
|
||||||
|
'fc_child_saved' => 'Child subscription saved!',
|
||||||
|
'btn_add_counter' => '➕ Add',
|
||||||
|
'btn_save_rights' => '💾 Save',
|
||||||
|
'fc_modal_settings_title' => 'Calendar Settings',
|
||||||
|
'fc_zone_label' => 'School Zone (Holidays)',
|
||||||
|
'fc_zone_a' => 'Zone A',
|
||||||
|
'fc_zone_b' => 'Zone B',
|
||||||
|
'fc_zone_c' => 'Zone C',
|
||||||
|
'fc_zone_other' => 'Other / Outside France',
|
||||||
|
'fc_zone_desc' => 'Determines the automatic school holiday calendar.',
|
||||||
|
'fc_care_modes_label' => 'Household Care Modes',
|
||||||
|
'fc_care_modes_desc' => 'Manage the global list of possible care modes for the family.',
|
||||||
|
'fc_add_mode_placeholder' => 'E.g., Nanny, Center, Grandparents...',
|
||||||
|
'fc_empty_kids_msg' => 'No children are configured in your household yet.',
|
||||||
|
'fc_link_settings' => 'Go to general settings',
|
||||||
|
'fc_settings_updated' => 'Settings updated successfully!',
|
||||||
|
'fc_first_day' => 'First day of the week',
|
||||||
|
'fc_helper_recap' => 'Helper Summary',
|
||||||
|
'fc_no_helper_leaves' => 'No leaves taken this month.',
|
||||||
|
'fc_col_date' => 'Date',
|
||||||
|
'fc_col_type' => 'Type',
|
||||||
|
'fc_col_duration' => 'Duration',
|
||||||
|
'fc_full_year' => 'Full year',
|
||||||
|
'fc_custom_period' => 'Custom period...',
|
||||||
|
'fc_worked_days' => 'Worked (Est.)',
|
||||||
|
'fc_tab_display' => '🖥️ Display',
|
||||||
|
'fc_display_title' => 'Display Settings',
|
||||||
|
'fc_display_desc' => 'Customize the default appearance and behavior of your calendar.',
|
||||||
|
'fc_default_view' => 'Default View',
|
||||||
|
'fc_working_hours' => 'Default Care Hours',
|
||||||
|
'fc_working_hours_desc' => 'Example: 08:30-18:00',
|
||||||
|
'fc_error' => 'Error: ',
|
||||||
|
'fc_leave_catalog_title' => 'Leave Catalog',
|
||||||
|
'fc_leave_catalog_desc' => 'Define the possible leave types for the whole family.',
|
||||||
|
'fc_add_leave_type' => 'Add a leave type',
|
||||||
|
'fc_col_code' => 'Code',
|
||||||
|
'fc_code_irreversible' => 'Irreversible',
|
||||||
|
'fc_col_label' => 'Label',
|
||||||
|
'fc_leave_label_ph' => 'Ex: PTO, Sick leave...',
|
||||||
|
'fc_quota_default' => 'Default quota',
|
||||||
|
'fc_reset_month' => 'Reset month',
|
||||||
|
'fc_leave_obsolete' => 'Obsolete',
|
||||||
|
'fc_leave_code_placeholder' => 'Code...',
|
||||||
|
'fc_leave_method_fixed' => 'Fixed',
|
||||||
|
'fc_leave_method_accumulated'=> 'Accrued',
|
||||||
|
'fc_date_format_placeholder'=> 'DD/MM',
|
||||||
|
'fc_date_format_title' => 'DD/MM Format',
|
||||||
|
'fc_table_code' => 'Code',
|
||||||
|
'fc_table_method' => 'Method',
|
||||||
|
'fc_table_quota' => 'Quota',
|
||||||
|
'fc_table_renewal' => 'Renewal',
|
||||||
|
'fc_label_carry_over' => 'Incl. carried over days (Expiring) ⚠️',
|
||||||
|
'fc_carry_over_note' => 'Portion of the overall balance that will expire on the deadline.',
|
||||||
|
'fc_carry_max' => 'Max Carry Over',
|
||||||
|
'fc_carry_deadline' => 'Expiration Month',
|
||||||
|
'fc_deadline_none' => 'None',
|
||||||
|
'fc_allowance_help' => 'Fixed: Annual quota. Accrued: Added per month.',
|
||||||
|
'fc_carry_max_short' => 'Max carry-over',
|
||||||
|
'fc_edit_leave' => '✏️ Edit:',
|
||||||
|
'fc_not_modifiable' => 'Not modifiable',
|
||||||
|
'fc_add_leave' => '+ Add',
|
||||||
|
'fc_err_code_label' => 'Code and Label are required.',
|
||||||
|
'fc_catalog_updated' => 'Catalog updated!',
|
||||||
|
'fc_balance_corrected' => 'Balance corrected!',
|
||||||
|
'fc_no_leave_configured' => 'No leave configured.',
|
||||||
|
'fc_confirm_del_catalog' => 'Permanently delete leave %s from the catalog? This will also remove it from members using it.',
|
||||||
|
'fc_confirm_del_member_leave'=> 'Do you really want to remove this leave for this member?',
|
||||||
|
|
||||||
|
// Calendar Legends
|
||||||
'leg_presence' => 'Pep presence',
|
'leg_presence' => 'Pep presence',
|
||||||
'leg_school_holidays' => 'School holidays',
|
'leg_school_holidays' => 'School holidays',
|
||||||
'leg_public_holiday' => 'Public holiday',
|
'leg_public_holiday' => 'Public holiday',
|
||||||
@@ -208,8 +366,10 @@ return [
|
|||||||
'leg_extra_off' => 'Extra off',
|
'leg_extra_off' => 'Extra off',
|
||||||
'leg_centre' => 'Centre',
|
'leg_centre' => 'Centre',
|
||||||
'leg_avis' => 'Notice',
|
'leg_avis' => 'Notice',
|
||||||
'leg_pep_sick' => 'Pep sick',
|
'fc_menu_sick' => 'Sick',
|
||||||
|
'care_nounou' => 'Nanny',
|
||||||
|
'care_centre' => 'Daycare',
|
||||||
|
'care_avis' => 'Grandparents',
|
||||||
'vac_toussaint' => 'All Saints\' holidays',
|
'vac_toussaint' => 'All Saints\' holidays',
|
||||||
'vac_noel' => 'Christmas holidays',
|
'vac_noel' => 'Christmas holidays',
|
||||||
'vac_hiver' => 'Winter holidays',
|
'vac_hiver' => 'Winter holidays',
|
||||||
@@ -218,12 +378,11 @@ return [
|
|||||||
'vac_ete' => 'Summer holidays',
|
'vac_ete' => 'Summer holidays',
|
||||||
'btn_off' => 'Off',
|
'btn_off' => 'Off',
|
||||||
'btn_extra' => 'Extra',
|
'btn_extra' => 'Extra',
|
||||||
|
'fc_alert_burn_days' => '⚠️ Risk of losing: %s d to take before end of cycle',
|
||||||
'fc_alert_burn_days' => '⚠️ Risk of losing: %s d to take before %s',
|
'fc_alert_burn_jra' => '⚠️ Alert: Carried over days expire at the end of this month!',
|
||||||
'fc_alert_burn_jra' => '⚠️ JRA: Only 2 days can be carried over after February!',
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// HOLIDAYS (TRIPS)
|
// 6. HOLIDAYS (TRIPS)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'hdl_title_list' => 'HouseHub - My Trips',
|
'hdl_title_list' => 'HouseHub - My Trips',
|
||||||
'hdl_title_detail' => 'HouseHub - Trip details',
|
'hdl_title_detail' => 'HouseHub - Trip details',
|
||||||
@@ -298,6 +457,8 @@ return [
|
|||||||
'hdl_to_place' => 'To place',
|
'hdl_to_place' => 'To place',
|
||||||
'hdl_from' => 'From',
|
'hdl_from' => 'From',
|
||||||
'hdl_to' => 'To',
|
'hdl_to' => 'To',
|
||||||
|
|
||||||
|
// Weather & Holidays specific
|
||||||
'weather_forecast' => 'Weather forecast',
|
'weather_forecast' => 'Weather forecast',
|
||||||
'weather_not_available' => 'Weather unavailable',
|
'weather_not_available' => 'Weather unavailable',
|
||||||
'temp_max' => 'Max temp.',
|
'temp_max' => 'Max temp.',
|
||||||
@@ -365,7 +526,7 @@ return [
|
|||||||
'to_place' => '📥 To place',
|
'to_place' => '📥 To place',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// BUDGET (GLOBAL & CATEGORIES)
|
// 7. BUDGET (GLOBAL, SUIVI, PREV, EPARGNE, PROVISIONS)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'budget_page_title' => 'HouseHub - Budget',
|
'budget_page_title' => 'HouseHub - Budget',
|
||||||
'budget_main_header' => 'Budget Management',
|
'budget_main_header' => 'Budget Management',
|
||||||
@@ -373,9 +534,16 @@ return [
|
|||||||
'budget_tab_prev' => 'Budget 2026',
|
'budget_tab_prev' => 'Budget 2026',
|
||||||
'budget_tab_savings' => 'Savings',
|
'budget_tab_savings' => 'Savings',
|
||||||
'budget_tab_recap' => 'Summary',
|
'budget_tab_recap' => 'Summary',
|
||||||
|
'budget_tab_provisions' => 'Provisions',
|
||||||
|
'budget_tab_kids' => 'Kids',
|
||||||
'budget_err_file_not_found' => 'Oops! File not found.',
|
'budget_err_file_not_found' => 'Oops! File not found.',
|
||||||
'budget_err_file_detail' => 'Check that the file views/%s.php exists.',
|
'budget_err_file_detail' => 'Check that the file views/%s.php exists.',
|
||||||
'budget_err_not_authorized' => 'Page not authorised.',
|
'budget_err_not_authorized' => 'Page not authorised.',
|
||||||
|
'bud_err_tech' => 'A technical error occurred.',
|
||||||
|
'bud_err_server' => 'Server error: ',
|
||||||
|
'bud_err_delete' => 'Error while deleting.',
|
||||||
|
|
||||||
|
// Categories (Budget)
|
||||||
'cat_income' => 'Income',
|
'cat_income' => 'Income',
|
||||||
'cat_fmcg' => 'Groceries (FMCG)',
|
'cat_fmcg' => 'Groceries (FMCG)',
|
||||||
'cat_fuel' => 'Fuel',
|
'cat_fuel' => 'Fuel',
|
||||||
@@ -384,7 +552,104 @@ return [
|
|||||||
'cat_others' => 'Other / Unexpected',
|
'cat_others' => 'Other / Unexpected',
|
||||||
'cat_savings' => 'Savings',
|
'cat_savings' => 'Savings',
|
||||||
|
|
||||||
// --- BUDGET: TRACKING ---
|
// Settings (Budget)
|
||||||
|
'budget_settings_title' => 'Budget Settings',
|
||||||
|
'bs_tab_accounts' => '💳 Accounts',
|
||||||
|
'bs_tab_categories' => '🏷️ Categories',
|
||||||
|
'bs_tab_rules' => '🤖 Import Rules',
|
||||||
|
'bs_tab_salaries' => '💼 Salaries',
|
||||||
|
'bs_tab_csv' => '📥 CSV Import',
|
||||||
|
'bs_empty_accounts' => 'No accounts configured.',
|
||||||
|
'bs_type_checking' => 'Checking',
|
||||||
|
'bs_type_savings' => 'Savings',
|
||||||
|
'bs_badge_default' => 'Default',
|
||||||
|
'bs_owner' => 'Owner',
|
||||||
|
'bs_shared_account' => 'Joint Account',
|
||||||
|
'bs_empty_categories' => 'No categories configured.',
|
||||||
|
'bs_type_income' => 'Income',
|
||||||
|
'bs_type_expense' => 'Expense',
|
||||||
|
'bs_type_savings_cat' => 'Savings',
|
||||||
|
'bs_empty_rules' => 'No import rules configured.',
|
||||||
|
'bs_empty_salaries' => 'No salary defined for the year',
|
||||||
|
'bs_salaries_year_desc' => 'Configurations for the year',
|
||||||
|
'bs_salary_net' => 'Net salary',
|
||||||
|
'bs_salary_fees' => 'Monthly payment / Fees',
|
||||||
|
'bs_add_account_title' => 'Add an account',
|
||||||
|
'bs_account_name_ph' => 'Name (e.g., Baby Savings)',
|
||||||
|
'bs_confirm_delete' => 'Are you sure you want to delete this account?',
|
||||||
|
'bs_add_category_title' => '+ Add a category',
|
||||||
|
'bs_cat_code_ph' => 'Code (e.g., FOOD)',
|
||||||
|
'bs_cat_label_ph' => 'Label (e.g., Groceries)',
|
||||||
|
'bs_cat_icon_ph' => 'Icon (e.g., 🛒)',
|
||||||
|
'bs_confirm_delete_cat' => 'Are you sure you want to delete this category?',
|
||||||
|
'bs_add_rule_title' => 'Add a rule',
|
||||||
|
'bs_rule_keyword_ph' => 'Keyword (e.g., WALMART)',
|
||||||
|
'bs_confirm_delete_rule'=> 'Are you sure you want to delete this rule?',
|
||||||
|
'bs_currency_label' => 'Household Currency',
|
||||||
|
'bs_currency_desc' => 'Main currency used for displaying your accounts and budgets.',
|
||||||
|
'bs_currency_updated' => 'Currency updated successfully!',
|
||||||
|
'bud_settings_dyn_title' => 'Dynamic Estimates (Children)',
|
||||||
|
'bud_dyn_nanny_fixed' => 'Nanny fixed salary (€)',
|
||||||
|
'bud_dyn_nanny_daily' => 'Daily maintenance fee (€)',
|
||||||
|
'bud_dyn_nanny_aid' => 'Government aid (€)',
|
||||||
|
'bud_dyn_nanny_cesu_avg' => 'Average CESU paid (€)',
|
||||||
|
'bud_dyn_school_meal' => 'School meal price (€)',
|
||||||
|
'bud_dyn_school_aftercare' => 'After-school care price (€)',
|
||||||
|
'bud_dyn_school_fullday' => 'Full day care (Wednesdays/Holidays) (€)',
|
||||||
|
'bud_dyn_pattern_title' => 'Standard school week',
|
||||||
|
'bud_audit_month' => 'Analyzed month: %02d/%d',
|
||||||
|
'bud_audit_nanny_abs' => '%s absences deducted : %dd',
|
||||||
|
'bud_audit_child' => '👦 %s :',
|
||||||
|
'bud_audit_school_fee' => 'Canteen (%dd) + Aftercare (%dd) + Center (%dd) = %.2f€',
|
||||||
|
'bud_audit_sick' => 'Sick days deducted : %dd',
|
||||||
|
'bud_audit_nanny_full' => '💼 %s : Fixed (%.2f€) + Fees (%dd x %.2f€ = %.2f€) - Aid (%.2f€) - CESU (%.2f€) = %.2f€',
|
||||||
|
|
||||||
|
// CSV Mapping (Budget)
|
||||||
|
'bs_csv_title' => 'CSV Import Format',
|
||||||
|
'bs_csv_desc' => 'Configure the columns that match your bank\'s export file.',
|
||||||
|
'bs_csv_delimiter' => 'Delimiter',
|
||||||
|
'bs_csv_delim_semi' => 'Semicolon (;)',
|
||||||
|
'bs_csv_delim_comma' => 'Comma (,)',
|
||||||
|
'bs_csv_delim_tab' => 'Tab',
|
||||||
|
'bs_csv_date_format' => 'Date Format',
|
||||||
|
'bs_csv_col_index' => 'Column Indexes',
|
||||||
|
'bs_csv_col_index_help' => '(Note: the 1st column is Number 0)',
|
||||||
|
'bs_csv_col_date' => 'Col #: Date',
|
||||||
|
'bs_csv_col_label' => 'Col #: Label / Description',
|
||||||
|
'bs_csv_amount_mgmt' => 'Amount Management',
|
||||||
|
'bs_csv_amount_single' => 'Single column (Positive = Income, Negative = Expense)',
|
||||||
|
'bs_csv_amount_split' => 'Two separate columns (Debit / Credit)',
|
||||||
|
'bs_csv_col_amount' => 'Col #: Amount',
|
||||||
|
'bs_csv_col_debit' => 'Col #: Debit',
|
||||||
|
'bs_csv_col_credit' => 'Col #: Credit',
|
||||||
|
'bs_csv_col_ref' => 'Col #: Unique Reference (Optional)',
|
||||||
|
'btn_save_format' => 'Save format',
|
||||||
|
'bs_csv_saved' => 'CSV format saved',
|
||||||
|
'bs_csv_settings_title' => 'Format Configuration',
|
||||||
|
'bs_csv_drop_title' => 'Drop your CSV file here',
|
||||||
|
'bs_csv_drop_desc' => 'or click to browse your files',
|
||||||
|
'bs_csv_err_type' => 'Error: Only .csv files are allowed.',
|
||||||
|
'bs_csv_file_ok' => 'File loaded',
|
||||||
|
'bs_csv_preview_title' => 'Columns Preview',
|
||||||
|
'bs_csv_empty_col' => 'Empty',
|
||||||
|
'bs_csv_no_data' => 'No transaction row found in this file.',
|
||||||
|
|
||||||
|
// Onboarding Budget
|
||||||
|
'bs_onb_title' => 'Welcome! Let\'s set up your budget:',
|
||||||
|
'bs_onb_desc' => 'Complete these steps so the app can calculate your remaining budget:',
|
||||||
|
'bs_onb_cat_link' => 'Categories',
|
||||||
|
'bs_onb_cat_desc' => 'Create at least one expense category.',
|
||||||
|
'bs_onb_acc_link' => 'Bank Accounts',
|
||||||
|
'bs_onb_acc_desc' => 'Add at least one checking or savings account.',
|
||||||
|
'bs_onb_sal_link' => 'Salaries',
|
||||||
|
'bs_onb_sal_desc' => 'Enter your income for the current year.',
|
||||||
|
'bs_onb_items_link' => 'Budget Items',
|
||||||
|
'bs_onb_items_desc' => 'Go to the Summary tab to list your fixed expenses (Rent, Insurance...).',
|
||||||
|
'bs_rule_search_ph' => '🔍 Search for a keyword (e.g. WALMART)...',
|
||||||
|
'error_loading_settings' => 'Error loading settings',
|
||||||
|
'bud_err_delete_system_cat' => 'Cannot delete a core system category.',
|
||||||
|
|
||||||
|
// Budget : Tracking
|
||||||
'bud_rem_school' => 'School (estimated remaining)',
|
'bud_rem_school' => 'School (estimated remaining)',
|
||||||
'bud_rem_fmcg' => 'Groceries (estimated remaining)',
|
'bud_rem_fmcg' => 'Groceries (estimated remaining)',
|
||||||
'bud_rem_fuel' => 'Fuel (estimated remaining)',
|
'bud_rem_fuel' => 'Fuel (estimated remaining)',
|
||||||
@@ -440,8 +705,9 @@ return [
|
|||||||
'bud_bar_actual' => 'Actual',
|
'bud_bar_actual' => 'Actual',
|
||||||
'bud_bar_theoretical' => 'Theoretical',
|
'bud_bar_theoretical' => 'Theoretical',
|
||||||
'bud_bar_max_cap' => 'Max capacity',
|
'bud_bar_max_cap' => 'Max capacity',
|
||||||
|
'bud_select_beneficiary' => 'Choose an expected income',
|
||||||
|
|
||||||
// --- BUDGET: RECAP ---
|
// Budget : Recap
|
||||||
'bud_recap_monthly_title' => 'Monthly summary',
|
'bud_recap_monthly_title' => 'Monthly summary',
|
||||||
'bud_recap_btn_add' => 'Add a cost / income',
|
'bud_recap_btn_add' => 'Add a cost / income',
|
||||||
'bud_planned_amount' => 'Planned amount',
|
'bud_planned_amount' => 'Planned amount',
|
||||||
@@ -481,39 +747,41 @@ return [
|
|||||||
'bud_label_regularization' => 'Regularisation',
|
'bud_label_regularization' => 'Regularisation',
|
||||||
'bud_reg_none' => 'None',
|
'bud_reg_none' => 'None',
|
||||||
'bud_recap_confirm_delete' => 'Are you sure you want to delete this item?',
|
'bud_recap_confirm_delete' => 'Are you sure you want to delete this item?',
|
||||||
'bud_err_delete' => 'Error while deleting.',
|
'bud_prev_monthly_target' => 'Target amount / Monthly payment (€)',
|
||||||
|
'bud_prev_transfer_dest' => 'Transfer destination',
|
||||||
|
'bud_prev_none' => 'No destination (Standard expense)',
|
||||||
|
|
||||||
// --- BUDGET: SAVINGS ---
|
// Budget : Savings
|
||||||
'bud_sav_add_month' => 'Enter a month',
|
'bud_sav_add_month' => 'Enter a month',
|
||||||
'bud_sav_add_one_month' => '+1 Month',
|
'bud_sav_add_one_month' => '+1 Month',
|
||||||
'bud_sav_no_data' => 'No data for %s.',
|
'bud_sav_no_data' => 'No data for %s.',
|
||||||
'bud_sav_post_month' => 'Category / Month',
|
'bud_sav_post_month' => 'Category / Month',
|
||||||
'bud_sav_from_date' => 'From %s',
|
'bud_sav_from_date' => 'From %s',
|
||||||
'bud_sav_edit_modal' => 'Edit with modal',
|
'bud_sav_edit_modal' => 'Edit this month',
|
||||||
'bud_sav_delete_month' => 'Delete entire month',
|
'bud_sav_delete_month' => 'Delete this month',
|
||||||
'bud_sav_total_bank' => 'Total bank',
|
'bud_sav_total_bank' => 'TOTAL BANK',
|
||||||
'bud_sav_extra' => 'Extra',
|
'bud_sav_extra' => 'UNALLOCATED (EXTRAS)',
|
||||||
'bud_sav_modal_title_add' => 'Enter a month',
|
'bud_sav_modal_title_add' => 'Add a month',
|
||||||
'bud_sav_modal_title_edit' => 'Edit:',
|
'bud_sav_modal_title_edit' => 'Edit',
|
||||||
'bud_sav_month_concerned' => 'Month concerned',
|
'bud_sav_month_concerned' => 'Target month',
|
||||||
'bud_sav_total_bank_eur' => 'Total in bank (€)',
|
'bud_sav_total_bank_eur' => 'Total in Bank (€)',
|
||||||
'bud_sav_ventilation' => 'Breakdown',
|
'bud_sav_ventilation' => 'Accounts breakdown',
|
||||||
'bud_sav_adj_help' => 'Use the adjustment (+/-) to recalculate automatically.',
|
'bud_sav_adj_help' => 'Enter adjustments (+/-) to auto-calculate.',
|
||||||
'bud_sav_add_line' => 'Line',
|
'bud_sav_add_line' => 'Add line',
|
||||||
'bud_sav_current' => 'Current',
|
'bud_sav_current' => 'Current',
|
||||||
'bud_sav_adjust' => 'Adj. (+/-)',
|
'bud_sav_adjust' => 'Adjustment',
|
||||||
'bud_sav_new' => 'New',
|
'bud_sav_new' => 'New',
|
||||||
'bud_sav_sum_mode_title' => 'Enable quick addition',
|
'bud_sav_sum_mode_title' => 'Toggle calculator',
|
||||||
'bud_sav_selection' => 'Selection:',
|
'bud_sav_selection' => 'Selection:',
|
||||||
'bud_sav_ph_name' => 'Name (e.g. Holidays)',
|
'bud_sav_ph_name' => 'Account name',
|
||||||
'bud_sav_saving' => 'Saving...',
|
'bud_sav_new_line' => 'New line',
|
||||||
'bud_err_tech' => 'A technical error occurred.',
|
'bud_sav_prompt_new_line' => 'Name of the new savings line:',
|
||||||
'bud_sav_confirm_delete_month' => 'Delete ALL of month %m for %o?',
|
'bud_sav_confirm_delete_month' => 'Are you sure you want to delete all data of %m for %o?',
|
||||||
'bud_sav_prompt_duplicate' => "Do you want to copy data from %s to %t1?\n\nEnter the new TOTAL bank balance (€) for %t2:",
|
'bud_sav_prompt_duplicate' => "Do you want to duplicate data from %s to %t1?\n\nEnter the new BANK TOTAL (€) for %t2:",
|
||||||
'bud_err_server' => 'Server error: ',
|
|
||||||
'bud_err_network_dup' => 'Network error during duplication.',
|
'bud_err_network_dup' => 'Network error during duplication.',
|
||||||
|
'bud_sav_saving' => 'Saving...',
|
||||||
|
|
||||||
// --- BUDGET: FORECAST ---
|
// Budget : Forecast
|
||||||
'bud_prev_incomes' => 'Income',
|
'bud_prev_incomes' => 'Income',
|
||||||
'bud_prev_person' => 'Person',
|
'bud_prev_person' => 'Person',
|
||||||
'bud_prev_salary' => 'Salary',
|
'bud_prev_salary' => 'Salary',
|
||||||
@@ -548,55 +816,93 @@ return [
|
|||||||
'bud_prev_confirm_copy' => "Do you want to copy data from %s to %t?\n\n⚠️ This will overwrite all values already present for %t.",
|
'bud_prev_confirm_copy' => "Do you want to copy data from %s to %t?\n\n⚠️ This will overwrite all values already present for %t.",
|
||||||
'bud_prev_confirm_transfers' => "Confirm that %p has made all their transfers for %m?\n\nThis will automatically update Savings.",
|
'bud_prev_confirm_transfers' => "Confirm that %p has made all their transfers for %m?\n\nThis will automatically update Savings.",
|
||||||
'bud_prev_label_name' => 'Name',
|
'bud_prev_label_name' => 'Name',
|
||||||
|
'bud_prev_target_ph' => 'E.g., 150',
|
||||||
|
'bud_prev_transfer_to' => 'To',
|
||||||
|
|
||||||
|
// Budget : Tricount / Advances
|
||||||
|
'bud_adv_title' => 'Advances & Family Tricount',
|
||||||
|
'bud_adv_add' => 'Add an Advance',
|
||||||
|
'bud_adv_edit' => 'Edit Advance',
|
||||||
|
'bud_adv_who_paid' => 'Who paid?',
|
||||||
|
'bud_adv_payer' => 'Payer',
|
||||||
|
'bud_adv_has_advanced' => '%s advanced:',
|
||||||
|
'bud_adv_cc_label' => 'on the joint account',
|
||||||
|
'bud_adv_livret_label' => 'from their savings account',
|
||||||
|
'bud_adv_cc_balance_title' => 'Joint Account Balance',
|
||||||
|
'bud_adv_owed_to' => '<strong>%s €</strong> are owed to <strong>%s</strong>',
|
||||||
|
'bud_adv_balanced' => 'Accounts are perfectly balanced!',
|
||||||
|
'bud_adv_saved_badge' => 'Savings',
|
||||||
|
'bud_adv_already_saved' => 'This money comes from a savings account',
|
||||||
|
'bud_adv_ph_desc' => 'e.g., Groceries, Electricity bill...',
|
||||||
|
'bud_adv_confirm_resolve' => 'Confirm the complete reimbursement of this advance?',
|
||||||
|
'bud_adv_confirm_delete' => 'Permanently delete this advance?',
|
||||||
|
|
||||||
|
// Budget : Provisions & Optimization
|
||||||
|
'budget_provisions_title' => 'Provisions & Cashflow Optimization',
|
||||||
|
'btn_optimize_cashflow' => 'Optimize My Savings',
|
||||||
|
'add_new_provision' => 'Anticipate an upcoming major expense',
|
||||||
|
'provision_label' => 'Expense description',
|
||||||
|
'provision_placeholder_wood' => 'E.g., Firewood order, Car service...',
|
||||||
|
'amount' => 'Amount',
|
||||||
|
'expected_date' => 'Expected date',
|
||||||
|
'upcoming_provisions_list' => 'Scheduled expenses',
|
||||||
|
'optimization_assistant_title' => 'Fortnightly Interest Optimization Assistant',
|
||||||
|
'success_add_provision' => 'Forecasted expense added successfully!',
|
||||||
|
'no_provisions' => 'No forecasted expenses registered at the moment.',
|
||||||
|
'savings_this_month' => 'Total savings to allocate this month',
|
||||||
|
'calculate' => 'Calculate optimal strategy',
|
||||||
|
'date_past_warning' => 'The selected date has already passed!',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// GIFTS
|
// 8. GIFTS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'gift_page_title' => 'HouseHub - Gift list',
|
'gift_page_title' => 'Gift List',
|
||||||
'gift_occ_tio' => 'Tió',
|
'gift_main_title' => 'Gifts of %s',
|
||||||
'gift_occ_noel' => 'Christmas',
|
'gift_filter_all_children' => 'All children',
|
||||||
'gift_occ_rois' => 'Three Kings',
|
'gift_filter_all_adults' => 'All adults',
|
||||||
'gift_occ_anniv' => 'Birthday',
|
'gift_add_gift' => 'Add',
|
||||||
'gift_occ_sant' => 'Name day',
|
'gift_empty_state_no_gifts' => 'No gifts recorded for this occasion.',
|
||||||
'gift_main_title' => 'Gift list %s',
|
'gift_empty_state_no_filter' => 'No gifts match your filters.',
|
||||||
'gift_aria_change_view' => 'Change view',
|
'gift_paid_by' => 'Paid by %s',
|
||||||
'gift_view_nadal' => 'Christmas',
|
'gift_liquidations' => 'Balance & Refunds',
|
||||||
'gift_view_anniv' => 'Birthdays',
|
'gift_no_debt' => 'Everything is settled, no debts!',
|
||||||
'gift_view_by_party' => 'View by occasion',
|
'gift_owes' => 'owes',
|
||||||
'gift_no_gifts' => 'No gifts recorded for %s in this view.',
|
'gift_view_matrix' => 'View detailed matrix',
|
||||||
'gift_add_gift' => 'Add a gift',
|
'gift_debtor' => 'Debtor',
|
||||||
'gift_paid_by' => 'paid by %s',
|
'gift_creditor' => 'Creditor',
|
||||||
'gift_summary_title' => 'Budget summary',
|
'gift_col_adult' => 'On behalf of',
|
||||||
'gift_col_adult' => 'Adult',
|
'gift_modal_payer' => 'Paid by',
|
||||||
'gift_col_child' => 'Child',
|
'gift_modal_gift_name' => 'Gift description',
|
||||||
'gift_col_party' => 'Occasion',
|
'gift_modal_price' => 'Estimated / paid price (€)',
|
||||||
'gift_debtor' => 'Debtor',
|
'gift_modal_link' => 'Product link (URL)',
|
||||||
'gift_creditor' => 'Creditor',
|
'gift_modal_title_add' => 'Add for %s',
|
||||||
'gift_liquidations' => 'Settlement',
|
'gift_modal_title_edit' => 'Edit gift',
|
||||||
'gift_no_debt' => 'No outstanding debts.',
|
'gift_confirm_delete' => 'Are you sure you want to delete this gift?',
|
||||||
'gift_owes' => 'owes',
|
'gift_settings_title' => 'Occasions Settings',
|
||||||
'gift_to' => 'to',
|
'gift_settings_add_title' => '+ Add an occasion',
|
||||||
'gift_detailed_list' => 'Detailed gift list',
|
'gift_settings_name_placeholder' => 'Name (e.g. Valentine\'s Day)',
|
||||||
'gift_col_gift' => 'Gift',
|
'gift_settings_date_placeholder' => 'MM-DD',
|
||||||
'gift_col_link' => 'Link',
|
'gift_settings_date_help' => 'Date (MM-DD) is optional. Leave empty for a floating date.',
|
||||||
'gift_modal_title_add' => 'Add a gift for %s',
|
'gift_settings_empty' => 'No occasion configured.',
|
||||||
'gift_modal_title_edit' => 'Edit gift',
|
'gift_settings_active' => 'Active',
|
||||||
'gift_modal_payer' => 'Paid by',
|
'gift_settings' => 'Settings',
|
||||||
'gift_modal_gift_name' => 'Gift name',
|
'gift_tab_occasions' => 'Occasions & Events',
|
||||||
'gift_modal_ph_name' => 'e.g. Lego Star Wars',
|
'gift_tab_proches' => 'Relatives (Outside household)',
|
||||||
'gift_modal_price' => 'Price (€)',
|
'gift_settings_icon' => 'Icon',
|
||||||
'gift_modal_link' => 'Link (optional)',
|
'gift_settings_date' => 'Date (MM-DD)',
|
||||||
'gift_confirm_delete' => 'Are you sure you want to delete this gift?',
|
'gift_settings_add_occ' => 'Add an occasion',
|
||||||
'gift_filter_all_children' => 'All children',
|
'gift_settings_add_proche' => 'Add a relative',
|
||||||
'gift_filter_all_adults' => 'All adults',
|
'gift_role_proche_adulte' => 'Adult (Uncle, friend...)',
|
||||||
'gift_empty_state_no_gifts' => 'No gifts yet.',
|
'gift_role_proche_enfant' => 'Child (Nephew, niece...)',
|
||||||
'gift_empty_state_no_filter' => 'No gifts match the filter.',
|
'gift_ph_proche_name' => 'First name',
|
||||||
'gift_view_matrix' => 'View detailed matrix',
|
'gift_role_label' => 'Role',
|
||||||
|
'gift_settings_saved' => 'Settings saved!',
|
||||||
|
'gift_settings_edit_occ' => 'Edit occasion',
|
||||||
|
'gift_settings_edit_proche' => 'Edit relative',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// GARAGE MANAGER
|
// 9. GARAGE
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'menu_garage' => 'Garage',
|
|
||||||
'garage_page_title' => 'HouseHub - Garage',
|
'garage_page_title' => 'HouseHub - Garage',
|
||||||
'garage_stat_vehicles' => 'Vehicles',
|
'garage_stat_vehicles' => 'Vehicles',
|
||||||
'garage_stat_maintenances' => 'Services',
|
'garage_stat_maintenances' => 'Services',
|
||||||
@@ -645,4 +951,79 @@ return [
|
|||||||
'garage_change_photo' => 'Change photo',
|
'garage_change_photo' => 'Change photo',
|
||||||
'garage_new_photo' => 'New photo',
|
'garage_new_photo' => 'New photo',
|
||||||
'garage_update_photo' => 'Update',
|
'garage_update_photo' => 'Update',
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 10. LISTS & GROCERIES
|
||||||
|
// ==========================================
|
||||||
|
'liste_placeholder_add' => 'Add an item...',
|
||||||
|
'liste_btn_add' => 'Add',
|
||||||
|
'liste_btn_uncheck_all' => 'Uncheck all',
|
||||||
|
'liste_btn_delete_picked' => 'Delete checked items',
|
||||||
|
'liste_btn_clear_all' => 'Clear list',
|
||||||
|
'liste_subtitle' => '{pending} to do · {cart} checked',
|
||||||
|
'liste_added' => 'Added',
|
||||||
|
'liste_updated' => 'Updated',
|
||||||
|
'liste_deleted' => 'Deleted',
|
||||||
|
'liste_already_in' => 'Already in the list',
|
||||||
|
'liste_confirm_clear' => 'Clear the entire list?',
|
||||||
|
'liste_confirm_delete_list' => 'Delete this list and all its items?',
|
||||||
|
'liste_history_title' => 'Recently used — click to add',
|
||||||
|
'liste_new_list' => 'New list',
|
||||||
|
'liste_list_name_placeholder'=> 'List name...',
|
||||||
|
'liste_create' => 'Create',
|
||||||
|
'liste_rename_list' => 'Rename',
|
||||||
|
'liste_new_name' => 'New name:',
|
||||||
|
'liste_list_deleted' => 'List deleted',
|
||||||
|
'liste_list_created' => 'List created',
|
||||||
|
'liste_list_renamed' => 'List renamed',
|
||||||
|
'liste_edit_placeholder' => 'Edit item:',
|
||||||
|
'liste_empty_state' => 'Create your first list using the + button above.',
|
||||||
|
'liste_settings_title' => 'Lists — history',
|
||||||
|
'liste_settings_intro' => 'Maximum number of items remembered in history (shared across lists).',
|
||||||
|
'liste_settings_history_max' => 'History size',
|
||||||
|
'liste_settings_history_hint'=> 'Between 1 and 50.',
|
||||||
|
'groceries_settings_saved' => 'Preferences saved.',
|
||||||
|
'groceries_settings_error' => 'Could not save:',
|
||||||
|
'groceries_err_duplicate' => 'This product is already on the list.',
|
||||||
|
|
||||||
|
// --- MODULE MEALS ---
|
||||||
|
'menu_meals' => 'Meals',
|
||||||
|
'mod_meals_name' => 'Meal Plan',
|
||||||
|
'mod_meals_desc' => 'Plan weekly meals for the whole family.',
|
||||||
|
'meals_board_title' => 'Meal Planner',
|
||||||
|
'meals_copy_lunch' => 'Copy entire lunch',
|
||||||
|
'meals_copy_dinner' => 'Copy entire dinner',
|
||||||
|
'meal_lunch' => 'Lunch',
|
||||||
|
'meal_dinner' => 'Dinner',
|
||||||
|
'btn_apply_all' => 'All',
|
||||||
|
'btn_copy_to' => 'Copy to...',
|
||||||
|
'modal_copy_title' => 'Copy to...',
|
||||||
|
'source_individual' => 'Individual meal',
|
||||||
|
'source_row' => 'Full meal',
|
||||||
|
'week_current' => 'Current week',
|
||||||
|
'week_next' => 'Next week',
|
||||||
|
'meals_no_members' => 'Please configure family members in the settings.',
|
||||||
|
'day_full_mon' => 'Monday', 'day_full_tue' => 'Tuesday', 'day_full_wed' => 'Wednesday',
|
||||||
|
'day_full_thu' => 'Thursday', 'day_full_fri' => 'Friday', 'day_full_sat' => 'Saturday', 'day_full_sun' => 'Sunday',
|
||||||
|
'meal_title' => 'Meal Planner',
|
||||||
|
'meal_edit_title' => 'Edit meal',
|
||||||
|
'meal_label_name' => 'Planned menu',
|
||||||
|
'meal_ph_name' => 'E.g. Homemade lasagna',
|
||||||
|
'meal_week' => 'Week',
|
||||||
|
'meal_empty_slot' => 'To be defined...',
|
||||||
|
'meal_current_week' => 'Current week',
|
||||||
|
'meal_saved' => 'Meal saved!',
|
||||||
|
'meal_btn_import_pdf' => 'Import Menu PDF',
|
||||||
|
'meal_import_title' => 'Import canteen menu',
|
||||||
|
'meal_import_child' => 'For which child?',
|
||||||
|
'meal_select_child' => 'Select child',
|
||||||
|
'meal_pdf_file' => 'PDF File (Convivio)',
|
||||||
|
'meal_drop_pdf' => 'Click to add PDF',
|
||||||
|
'meal_import_success' => 'Menus imported successfully!',
|
||||||
|
'mod_food_name' => 'Kitchen & Groceries',
|
||||||
|
'food_tab_meals' => 'Weekly Menu',
|
||||||
|
'food_tab_list' => 'Shopping List',
|
||||||
|
'menu_food' => 'Food',
|
||||||
|
'mod_food_desc' => 'Manage your weekly menus and shared shopping lists.',
|
||||||
|
'cta_food' => 'Cook up',
|
||||||
];
|
];
|
||||||
+548
-185
@@ -1,30 +1,23 @@
|
|||||||
<?php
|
<?php
|
||||||
// Dictionnaire Français
|
// Dictionnaire Français - HouseHub (Optimisé & Dédupliqué)
|
||||||
return [
|
return [
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// TEST AUTO
|
// 1. GLOBAL, NAVIGATION & ACTIONS
|
||||||
// ==========================================
|
|
||||||
'test_running' => 'Tests en cours...',
|
|
||||||
'test_passed' => 'Test réussi',
|
|
||||||
'test_failed' => 'Test échoué',
|
|
||||||
'tests_title' => 'Laboratoire de Tests',
|
|
||||||
'tests_run_budget' => 'Tester le Budget',
|
|
||||||
'tests_run_all' => 'Lancer tous les tests',
|
|
||||||
'tests_copy_report' => 'Copier le rapport',
|
|
||||||
'tests_waiting' => 'En attente du lancement...',
|
|
||||||
'tests_report_copied' => 'Rapport copié dans le presse-papier !',
|
|
||||||
'tests_live_preview' => 'Aperçu en direct',
|
|
||||||
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// GLOBAL & NAVIGATION
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'menu_home' => 'Accueil',
|
'menu_home' => 'Accueil',
|
||||||
'menu_calendar' => 'Calendrier',
|
'menu_calendar' => 'Calendrier',
|
||||||
'menu_budget' => 'Budget',
|
'menu_budget' => 'Budget',
|
||||||
'menu_holidays' => 'Voyages',
|
'menu_holidays' => 'Voyages',
|
||||||
'menu_gifts' => 'Cadeaux',
|
'menu_gifts' => 'Cadeaux',
|
||||||
|
'menu_garage' => 'Garage',
|
||||||
|
'menu_memo' => 'Notes',
|
||||||
|
'menu_todo' => 'Todo',
|
||||||
|
'menu_liste' => 'Listes',
|
||||||
|
'menu_groceries' => 'Courses',
|
||||||
'menu_calendar_ios' => 'Calendrier iOS',
|
'menu_calendar_ios' => 'Calendrier iOS',
|
||||||
|
'menu_printvault' => 'PrintVault',
|
||||||
|
'menu_planka' => 'Planka',
|
||||||
|
|
||||||
'nav_home' => 'Accueil',
|
'nav_home' => 'Accueil',
|
||||||
'nav_calendar' => 'Calendrier',
|
'nav_calendar' => 'Calendrier',
|
||||||
'nav_budget' => 'Budget',
|
'nav_budget' => 'Budget',
|
||||||
@@ -32,8 +25,6 @@ return [
|
|||||||
'nav_gifts' => 'Cadeaux',
|
'nav_gifts' => 'Cadeaux',
|
||||||
'aria_menu' => 'Menu mobile',
|
'aria_menu' => 'Menu mobile',
|
||||||
|
|
||||||
|
|
||||||
// Boutons et actions globales
|
|
||||||
'actions' => 'Actions',
|
'actions' => 'Actions',
|
||||||
'edit' => 'Modifier',
|
'edit' => 'Modifier',
|
||||||
'delete' => 'Supprimer',
|
'delete' => 'Supprimer',
|
||||||
@@ -43,28 +34,33 @@ return [
|
|||||||
'btn_cancel' => 'Annuler',
|
'btn_cancel' => 'Annuler',
|
||||||
'btn_delete' => 'Supprimer',
|
'btn_delete' => 'Supprimer',
|
||||||
'btn_close' => 'Fermer',
|
'btn_close' => 'Fermer',
|
||||||
|
'btn_add' => 'Ajouter',
|
||||||
'btn_back' => '◀ Retour',
|
'btn_back' => '◀ Retour',
|
||||||
'btn_edit_bases' => '⚙️ Modifier les bases',
|
'btn_edit_bases' => '⚙️ Modifier les bases',
|
||||||
'btn_edit' => 'Modifier',
|
'btn_edit' => 'Modifier',
|
||||||
'error_occured' => 'Une erreur est survenue.',
|
|
||||||
|
|
||||||
// Mots génériques & Erreurs
|
|
||||||
'error' => 'Erreur.',
|
'error' => 'Erreur.',
|
||||||
|
'error_occured' => 'Une erreur est survenue.',
|
||||||
|
'error_network' => 'Erreur réseau',
|
||||||
|
'error_generic' => 'Une erreur est survenue',
|
||||||
'confirm_delete' => 'Es-tu sûr de vouloir supprimer cet élément ?',
|
'confirm_delete' => 'Es-tu sûr de vouloir supprimer cet élément ?',
|
||||||
|
'loading' => 'Chargement...',
|
||||||
|
'date' => 'Date',
|
||||||
|
'settings' => 'Paramètres',
|
||||||
|
|
||||||
// Mois & Jours
|
// Mois & Jours
|
||||||
'month_01' => 'Janvier', 'month_02' => 'Février', 'month_03' => 'Mars',
|
'month_01' => 'Janvier', 'month_02' => 'Février', 'month_03' => 'Mars', 'month_04' => 'Avril', 'month_05' => 'Mai', 'month_06' => 'Juin',
|
||||||
'month_04' => 'Avril', 'month_05' => 'Mai', 'month_06' => 'Juin',
|
'month_07' => 'Juillet', 'month_08' => 'Août', 'month_09' => 'Septembre', 'month_10' => 'Octobre', 'month_11' => 'Novembre', 'month_12' => 'Décembre',
|
||||||
'month_07' => 'Juillet', 'month_08' => 'Août', 'month_09' => 'Septembre',
|
'month_jan' => 'Janvier', 'month_feb' => 'Février', 'month_mar' => 'Mars', 'month_apr' => 'Avril', 'month_may' => 'Mai', 'month_jun' => 'Juin',
|
||||||
'month_10' => 'Octobre', 'month_11' => 'Novembre','month_12' => 'Décembre',
|
'month_jul' => 'Juillet', 'month_aug' => 'Août', 'month_sep' => 'Septembre', 'month_oct' => 'Octobre', 'month_nov' => 'Novembre', 'month_dec' => 'Décembre',
|
||||||
'day_mon' => 'Lun', 'day_tue' => 'Mar', 'day_wed' => 'Mer',
|
'day_mon' => 'Lun', 'day_tue' => 'Mar', 'day_wed' => 'Mer', 'day_thu' => 'Jeu', 'day_fri' => 'Ven', 'day_sat' => 'Sam', 'day_sun' => 'Dim',
|
||||||
'day_thu' => 'Jeu', 'day_fri' => 'Ven', 'day_sat' => 'Sam', 'day_sun' => 'Dim',
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// AUTHENTIFICATION / LOGIN
|
// 2. AUTHENTIFICATION / LOGIN
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'login_title' => 'Connexion - HouseHub',
|
'login_title' => 'Connexion - HouseHub',
|
||||||
'login_header' => 'Connexion',
|
'login_header' => 'Connexion',
|
||||||
|
'login_subtitle' => 'Veuillez vous identifier pour continuer',
|
||||||
'label_username' => 'Identifiant',
|
'label_username' => 'Identifiant',
|
||||||
'label_password' => 'Mot de passe',
|
'label_password' => 'Mot de passe',
|
||||||
'placeholder_username' => 'ex: pacha',
|
'placeholder_username' => 'ex: pacha',
|
||||||
@@ -72,18 +68,19 @@ return [
|
|||||||
'btn_login_submit' => 'Se connecter',
|
'btn_login_submit' => 'Se connecter',
|
||||||
'logout' => 'Déconnexion',
|
'logout' => 'Déconnexion',
|
||||||
'btn_logout' => 'Déconnexion',
|
'btn_logout' => 'Déconnexion',
|
||||||
|
'login_remember_me' => 'Se souvenir de moi',
|
||||||
'error_missing_fields' => 'Champs obligatoires.',
|
'error_missing_fields' => 'Champs obligatoires.',
|
||||||
'error_invalid_credentials' => 'Identifiants incorrects.',
|
'error_invalid_credentials' => 'Identifiants incorrects.',
|
||||||
'login_subtitle' => 'Veuillez vous identifier pour continuer',
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// ACCUEIL / INDEX
|
// 3. ACCUEIL & DASHBOARD
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'home_title' => 'HouseHub - Accueil',
|
'home_title' => 'HouseHub - Accueil',
|
||||||
'home_welcome' => 'Bienvenue sur HouseHub',
|
'home_welcome' => 'Bienvenue sur HouseHub',
|
||||||
'home_subtitle' => "Centre de contrôle de l'organisation familiale.",
|
'home_subtitle' => "Centre de contrôle de l'organisation familiale.",
|
||||||
'home_logged_as' => 'Connecté en tant que',
|
'home_logged_as' => 'Connecté en tant que',
|
||||||
'home_modules_title' => 'Modules',
|
'home_modules_title' => 'Modules',
|
||||||
|
|
||||||
'mod_calendar_name' => 'Calendrier Familial',
|
'mod_calendar_name' => 'Calendrier Familial',
|
||||||
'mod_calendar_desc' => "Gérez les congés, les modes de garde et l'organisation hebdomadaire.",
|
'mod_calendar_desc' => "Gérez les congés, les modes de garde et l'organisation hebdomadaire.",
|
||||||
'mod_holidays_name' => 'Vacances',
|
'mod_holidays_name' => 'Vacances',
|
||||||
@@ -96,49 +93,19 @@ return [
|
|||||||
'mod_garage_desc' => 'Suivez l\'entretien de vos véhicules, les pièces utilisées et les coûts.',
|
'mod_garage_desc' => 'Suivez l\'entretien de vos véhicules, les pièces utilisées et les coûts.',
|
||||||
'mod_memo_name' => 'Notes',
|
'mod_memo_name' => 'Notes',
|
||||||
'mod_memo_desc' => 'Vos mémos, recettes, instructions et infos diverses avec Markdown et pièces jointes.',
|
'mod_memo_desc' => 'Vos mémos, recettes, instructions et infos diverses avec Markdown et pièces jointes.',
|
||||||
'menu_memo' => 'Notes',
|
|
||||||
'mod_todo_name' => 'Todo',
|
'mod_todo_name' => 'Todo',
|
||||||
'mod_todo_desc' => 'Gérez vos tâches, listes de courses et rappels en famille.',
|
'mod_todo_desc' => 'Gérez vos tâches, listes de courses et rappels en famille.',
|
||||||
'mod_calendar_ios_name' => 'Calendrier iOS',
|
|
||||||
'mod_calendar_ios_desc' => 'Synchronisez vos événements HouseHub avec votre calendrier iPhone (iCloud CalDAV).',
|
|
||||||
'menu_todo' => 'Todo',
|
|
||||||
'mod_groceries_name' => 'Courses',
|
|
||||||
'mod_groceries_desc' => 'Liste partagée : ajoutez des produits et cochez-les quand ils sont dans le caddie.',
|
|
||||||
'menu_groceries' => 'Courses',
|
|
||||||
// Module Liste (remplace Courses)
|
|
||||||
'mod_liste_name' => 'Listes',
|
'mod_liste_name' => 'Listes',
|
||||||
'mod_liste_desc' => 'Listes partagées nommées — courses, bricolage, pharmacie… Autant de listes que nécessaire.',
|
'mod_liste_desc' => 'Listes partagées nommées — courses, bricolage, pharmacie… Autant de listes que nécessaire.',
|
||||||
'menu_liste' => 'Listes',
|
'mod_groceries_name' => 'Courses',
|
||||||
'liste_placeholder_add' => 'Ajouter un article…',
|
'mod_groceries_desc' => 'Liste partagée : ajoutez des produits et cochez-les quand ils sont dans le caddie.',
|
||||||
'liste_btn_add' => 'Ajouter',
|
'mod_calendar_ios_name' => 'Calendrier iOS',
|
||||||
'liste_btn_uncheck_all' => 'Tout décocher',
|
'mod_calendar_ios_desc' => 'Synchronisez vos événements HouseHub avec votre calendrier iPhone (iCloud CalDAV).',
|
||||||
'liste_btn_delete_picked' => 'Supprimer les cochés',
|
'mod_printvault_name'=> 'PrintVault',
|
||||||
'liste_btn_clear_all' => 'Vider la liste',
|
'mod_printvault_desc'=> 'Gérez vos fichiers d\'impression 3D — STL, 3MF, GCode avec viewer interactif.',
|
||||||
'liste_subtitle' => '{pending} à faire · {cart} cochés',
|
'mod_planka_name' => 'Planka',
|
||||||
'liste_added' => 'Ajouté',
|
'mod_planka_desc' => 'Gérez vos projets en mode Kanban avec Planka.',
|
||||||
'liste_updated' => 'Modifié',
|
|
||||||
'liste_deleted' => 'Supprimé',
|
|
||||||
'liste_already_in' => 'Déjà dans la liste',
|
|
||||||
'liste_confirm_clear' => 'Vider toute la liste ?',
|
|
||||||
'liste_confirm_delete_list' => 'Supprimer cette liste et tous ses articles ?',
|
|
||||||
'liste_history_title' => 'Récemment utilisés — clic pour rajouter',
|
|
||||||
'liste_new_list' => 'Nouvelle liste',
|
|
||||||
'liste_list_name_placeholder'=> 'Nom de la liste…',
|
|
||||||
'liste_create' => 'Créer',
|
|
||||||
'liste_rename_list' => 'Renommer',
|
|
||||||
'liste_new_name' => 'Nouveau nom :',
|
|
||||||
'liste_list_deleted' => 'Liste supprimée',
|
|
||||||
'liste_list_created' => 'Liste créée',
|
|
||||||
'liste_list_renamed' => 'Liste renommée',
|
|
||||||
'liste_edit_placeholder' => 'Modifier l\'article :',
|
|
||||||
'liste_empty_state' => 'Créez votre première liste avec le bouton + ci-dessus.',
|
|
||||||
'liste_settings_title' => 'Listes — historique',
|
|
||||||
'liste_settings_intro' => 'Nombre maximum d\'articles mémorisés dans l\'historique (commun à toutes les listes).',
|
|
||||||
'liste_settings_history_max' => 'Taille de l\'historique',
|
|
||||||
'liste_settings_history_hint'=> 'Entre 1 et 50.',
|
|
||||||
'groceries_settings_saved' => 'Préférences enregistrées.',
|
|
||||||
'groceries_settings_error' => 'Impossible d\'enregistrer :',
|
|
||||||
'groceries_err_duplicate' => 'Ce produit est déjà dans la liste.',
|
|
||||||
'cta_open' => 'Ouvrir',
|
'cta_open' => 'Ouvrir',
|
||||||
'cta_explore' => 'Explorer',
|
'cta_explore' => 'Explorer',
|
||||||
'cta_view_lists' => 'Déballer',
|
'cta_view_lists' => 'Déballer',
|
||||||
@@ -148,16 +115,124 @@ return [
|
|||||||
'cta_check' => 'Cocher',
|
'cta_check' => 'Cocher',
|
||||||
'cta_cart' => 'Faire les courses',
|
'cta_cart' => 'Faire les courses',
|
||||||
'cta_sync' => 'Synchroniser',
|
'cta_sync' => 'Synchroniser',
|
||||||
'mod_printvault_name' => 'PrintVault',
|
|
||||||
'mod_printvault_desc' => 'Gérez vos fichiers d\'impression 3D — STL, 3MF, GCode avec viewer interactif.',
|
|
||||||
'menu_printvault' => 'PrintVault',
|
|
||||||
'cta_print' => 'Imprimer',
|
'cta_print' => 'Imprimer',
|
||||||
'mod_planka_name' => 'Planka',
|
|
||||||
'mod_planka_desc' => 'Gérez vos projets en mode Kanban avec Planka.',
|
|
||||||
'menu_planka' => 'Planka',
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// FAMILY CALENDAR
|
// 4. PARAMÈTRES (SETTINGS)
|
||||||
|
// ==========================================
|
||||||
|
'set_title_page' => '⚙️ Paramètres',
|
||||||
|
'set_title_lang' => '🌐 Langue / Language',
|
||||||
|
'set_title_modules' => '🧩 Modules actifs',
|
||||||
|
'set_desc_modules' => 'Choisissez les modules visibles dans la navigation.',
|
||||||
|
'set_title_ios' => '📱 Intégration Calendrier iOS (CalDAV)',
|
||||||
|
'set_desc_ios' => 'Configurez ici votre calendrier iCloud pour synchroniser les événements.',
|
||||||
|
'set_label_apple_id' => 'Identifiant Apple (email iCloud)',
|
||||||
|
'set_label_apple_pwd' => 'Mot de passe d\'app Apple',
|
||||||
|
'set_label_apple_url' => 'URL du calendrier CalDAV',
|
||||||
|
'set_btn_test_ios' => 'Tester la connexion',
|
||||||
|
'set_btn_disconnect_ios' => 'Déconnecter',
|
||||||
|
'set_title_bg' => '🖼️ Image d\'accueil',
|
||||||
|
'set_desc_bg' => 'Photo de fond affichée sur la page d\'accueil.',
|
||||||
|
'set_badge_custom_bg' => 'Image personnalisée',
|
||||||
|
'set_label_new_bg' => 'Nouvelle image (jpg, png, webp — max 10 Mo)',
|
||||||
|
'set_btn_reset_bg' => '🔄 Remettre l\'image par défaut',
|
||||||
|
'set_confirm_reset_bg' => 'Revenir à l\'image par défaut ?',
|
||||||
|
'set_title_profile' => '👤 Mon profil',
|
||||||
|
'set_label_username' => 'Nom d\'utilisateur',
|
||||||
|
'set_label_display_name' => 'Prénom / Pseudo affiché',
|
||||||
|
'set_label_profile_color' => 'Couleur thématique de mon profil',
|
||||||
|
'set_desc_profile_color' => 'Cette couleur sera utilisée pour vos grilles de budget, calendriers et indicateurs personnels.',
|
||||||
|
'set_title_pwd' => '🔒 Changer le mot de passe',
|
||||||
|
'set_label_cur_pwd' => 'Mot de passe actuel',
|
||||||
|
'set_label_new_pwd' => 'Nouveau mot de passe',
|
||||||
|
'set_label_confirm_pwd' => 'Confirmer',
|
||||||
|
'set_btn_edit' => 'Modifier',
|
||||||
|
'set_title_family' => '🏠 Mon espace familial',
|
||||||
|
'set_label_family_name' => 'Nom de l\'espace',
|
||||||
|
'set_desc_invite' => 'Code d\'invitation — partagez-le pour inviter quelqu\'un',
|
||||||
|
'set_title_copy' => 'Cliquer pour copier',
|
||||||
|
'set_msg_copied' => '✓ Copié !',
|
||||||
|
'set_btn_new_code' => '🔄 Nouveau code',
|
||||||
|
'set_confirm_new_code' => 'Regénérer le code ? L\'ancien sera invalidé.',
|
||||||
|
'set_desc_register_link' => 'Lien d\'inscription :',
|
||||||
|
'set_title_members' => 'Membres',
|
||||||
|
'set_badge_admin' => 'Admin',
|
||||||
|
'set_title_attached' => '🧸 Profils rattachés',
|
||||||
|
'set_desc_attached' => 'Membres gérés par le foyer n\'ayant pas d\'accès direct.',
|
||||||
|
'set_empty_attached' => 'Aucun profil rattaché.',
|
||||||
|
'set_link_admin' => '→ Panneau d\'administration',
|
||||||
|
'set_kids_title' => 'Enfants du foyer',
|
||||||
|
'set_kids_desc' => 'Gérez la liste des enfants pour les associer à l\'épargne, aux cadeaux, etc.',
|
||||||
|
'set_add_kid_placeholder' => 'Prénom de l\'enfant',
|
||||||
|
'set_btn_add_kid' => 'Ajouter',
|
||||||
|
'set_err_kid_name' => 'Le prénom est requis.',
|
||||||
|
'set_confirm_del_kid' => 'Supprimer cet enfant ? (Son historique financier restera visible dans les archives).',
|
||||||
|
'set_no_kids' => 'Aucun enfant configuré pour ce foyer.',
|
||||||
|
'set_family_config' => 'Configuration du foyer',
|
||||||
|
'set_global_params' => 'Paramètres globaux',
|
||||||
|
'set_care_modes' => 'Modes de garde usuels',
|
||||||
|
'set_add_care_mode' => 'Ajouter un mode',
|
||||||
|
'btn_save_config' => 'Enregistrer',
|
||||||
|
|
||||||
|
// Labels Profils
|
||||||
|
'label_firstname' => 'Prénom',
|
||||||
|
'placeholder_firstname_ex' => 'Ex: Pol, Carole...',
|
||||||
|
'label_role' => 'Rôle',
|
||||||
|
'role_child' => '👶 Enfant',
|
||||||
|
'role_child_icon' => '👶 Enfant',
|
||||||
|
'role_helper' => '💼 Intervenant',
|
||||||
|
'role_helper_icon' => '💼 Intervenant',
|
||||||
|
'role_relative' => '👵 Proche',
|
||||||
|
'role_relative_icon' => '👵 Proche',
|
||||||
|
'role_unknown' => 'Inconnu',
|
||||||
|
'label_color' => 'Couleur',
|
||||||
|
'change_color' => 'Changer la couleur',
|
||||||
|
'confirm_delete_profile' => 'Supprimer définitivement ce profil ?',
|
||||||
|
|
||||||
|
// Messages Settings
|
||||||
|
'error_csrf' => 'Session invalide (CSRF). Rechargez la page.',
|
||||||
|
'set_err_min_module' => 'Vous devez garder au moins un module actif.',
|
||||||
|
'set_success_modules' => 'Modules mis à jour avec succès.',
|
||||||
|
'set_success_lang' => 'Langue mise à jour.',
|
||||||
|
'set_err_empty_name' => 'Le prénom ne peut pas être vide.',
|
||||||
|
'set_success_profile' => 'Profil mis à jour avec succès.',
|
||||||
|
'set_err_wrong_pwd' => 'Mot de passe actuel incorrect.',
|
||||||
|
'set_err_pwd_length' => 'Le nouveau mot de passe doit faire au moins 6 caractères.',
|
||||||
|
'set_err_pwd_match' => 'Les mots de passe ne correspondent pas.',
|
||||||
|
'set_success_pwd' => 'Mot de passe modifié avec succès.',
|
||||||
|
'set_success_family' => 'Nom de l\'espace mis à jour.',
|
||||||
|
'set_success_invite' => 'Nouveau code d\'invitation généré.',
|
||||||
|
'set_err_bg_format' => 'Format non supporté (jpg, png, webp).',
|
||||||
|
'set_success_bg' => 'Image d\'accueil mise à jour.',
|
||||||
|
'set_err_upload' => 'Erreur lors de l\'upload.',
|
||||||
|
'set_success_bg_reset' => 'Image d\'accueil réinitialisée.',
|
||||||
|
'set_err_db_unavailable' => 'Base famille indisponible.',
|
||||||
|
'set_err_ios_missing' => 'Merci de renseigner identifiant iCloud, mot de passe d\'app et URL CalDAV.',
|
||||||
|
'set_success_ios' => 'Connexion calendrier iOS enregistrée.',
|
||||||
|
'set_success_ios_auto' => ' URL du calendrier détectée automatiquement.',
|
||||||
|
'set_err_ios_save' => 'Impossible d\'enregistrer la connexion iOS : ',
|
||||||
|
'set_err_ios_none' => 'Aucune connexion iOS configurée.',
|
||||||
|
'set_success_ios_test' => 'Connexion iCloud CalDAV valide (HTTP %s).',
|
||||||
|
'set_err_ios_test' => 'Test connexion échoué (HTTP %s). Vérifiez vos identifiants.',
|
||||||
|
'set_err_ios_test_fail' => 'Test connexion impossible : ',
|
||||||
|
'set_success_ios_del' => 'Connexion iOS supprimée.',
|
||||||
|
'set_success_profile_add' => 'Profil de %s ajouté avec succès.',
|
||||||
|
'set_err_name_required' => 'Le prénom est obligatoire.',
|
||||||
|
'set_success_profile_del' => 'Profil supprimé.',
|
||||||
|
'set_success_color' => 'Couleur mise à jour !',
|
||||||
|
'set_success_color_profile' => 'Couleur du profil mise à jour.',
|
||||||
|
'set_success_color_update' => 'Couleur mise à jour !',
|
||||||
|
'color_updated' => 'Couleur mise à jour !',
|
||||||
|
'settings_profile_added' => 'Profil de %s ajouté avec succès.',
|
||||||
|
'settings_err_name_required' => 'Le prénom est obligatoire.',
|
||||||
|
'settings_attached_profiles_title' => 'Profils rattachés',
|
||||||
|
'settings_attached_profiles_desc' => 'Membres gérés par le foyer (Enfants, Nounous, Proches) n\'ayant pas d\'accès direct.',
|
||||||
|
'settings_no_attached_profiles' => 'Aucun profil rattaché.',
|
||||||
|
'placeholder_apple_id' => 'nom@icloud.com',
|
||||||
|
'placeholder_apple_pwd' => 'xxxx-xxxx-xxxx-xxxx',
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 5. CALENDRIER FAMILIAL
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'fc_page_title' => 'HouseHub - Calendrier',
|
'fc_page_title' => 'HouseHub - Calendrier',
|
||||||
'fc_main_header' => 'Calendrier Familial',
|
'fc_main_header' => 'Calendrier Familial',
|
||||||
@@ -182,6 +257,7 @@ return [
|
|||||||
'fc_weekly_planning' => 'Planning hebdomadaire',
|
'fc_weekly_planning' => 'Planning hebdomadaire',
|
||||||
'fc_col_month' => 'Mois',
|
'fc_col_month' => 'Mois',
|
||||||
'fc_col_week_short' => 'Sem.',
|
'fc_col_week_short' => 'Sem.',
|
||||||
|
'fc_col_week' => 'Semaine',
|
||||||
'fc_col_av' => 'Av.',
|
'fc_col_av' => 'Av.',
|
||||||
'fc_col_use' => 'Use',
|
'fc_col_use' => 'Use',
|
||||||
'fc_legend_title' => 'Légende',
|
'fc_legend_title' => 'Légende',
|
||||||
@@ -192,14 +268,96 @@ return [
|
|||||||
'fc_unit_days' => 'jours',
|
'fc_unit_days' => 'jours',
|
||||||
'fc_menu_carole' => 'Congés Carole',
|
'fc_menu_carole' => 'Congés Carole',
|
||||||
'fc_clear' => 'Effacer',
|
'fc_clear' => 'Effacer',
|
||||||
'fc_menu_kids_leaves' => 'Congés Enfants',
|
'fc_menu_kids_leaves' => 'Congés Parents',
|
||||||
'fc_err_save_snap' => 'Erreur lors de la sauvegarde du correctif.',
|
'fc_err_save_snap' => 'Erreur lors de la sauvegarde du correctif.',
|
||||||
'fc_err_fetch_gov' => 'Erreur lors de la connexion à l\'API gouvernementale.',
|
'fc_err_fetch_gov' => 'Erreur lors de la connexion à l\'API gouvernementale.',
|
||||||
'fc_err_no_data_gov' => 'Aucune donnée trouvée sur l\'API du gouvernement pour cette année.',
|
'fc_err_no_data_gov' => 'Aucune donnée trouvée sur l\'API du gouvernement pour cette année.',
|
||||||
'fc_err_action' => 'Erreur lors de l\'action : ',
|
'fc_err_action' => 'Erreur lors de l\'action : ',
|
||||||
|
'fc_err_select_leave' => 'Veuillez sélectionner un type de congé.',
|
||||||
'fc_today_short' => 'Auj.',
|
'fc_today_short' => 'Auj.',
|
||||||
'fc_today_title' => "Aujourd'hui",
|
'fc_today_title' => "Aujourd'hui",
|
||||||
|
'fc_tab_foyer' => '🏡 Foyer',
|
||||||
|
'fc_tab_members' => '👥 Membres & Droits',
|
||||||
|
'fc_select_member' => 'Sélectionner un membre :',
|
||||||
|
'fc_role_adult' => 'Adulte',
|
||||||
|
'fc_role_child' => 'Enfant',
|
||||||
|
'fc_role_helper' => 'Intervenant',
|
||||||
|
'fc_role_unknown' => 'Inconnu',
|
||||||
|
'fc_care_modes_title' => 'Modes de Garde Par Défaut',
|
||||||
|
'fc_care_modes_desc' => 'Quels modes de garde s\'appliquent à cet enfant ?',
|
||||||
|
'fc_no_care_mode' => 'Aucun mode de garde configuré dans le foyer.',
|
||||||
|
'fc_matrix_title' => 'Matrice des Congés',
|
||||||
|
'fc_matrix_desc' => 'Définissez les quotas et la date de renouvellement.',
|
||||||
|
'fc_matrix_code' => 'Code',
|
||||||
|
'fc_matrix_date' => 'Date Reset',
|
||||||
|
'fc_matrix_empty' => 'Aucun compteur défini.',
|
||||||
|
'fc_matrix_saved' => 'Matrice enregistrée !',
|
||||||
|
'fc_child_saved' => 'Sauvegardé !',
|
||||||
|
'fc_modal_settings_title' => 'Paramètres du calendrier',
|
||||||
|
'fc_zone_label' => 'Zone scolaire',
|
||||||
|
'fc_zone_a' => 'Zone A',
|
||||||
|
'fc_zone_b' => 'Zone B',
|
||||||
|
'fc_zone_c' => 'Zone C',
|
||||||
|
'fc_zone_other' => 'Autre / Hors France',
|
||||||
|
'fc_zone_desc' => 'Permet d\'afficher correctement les vacances scolaires.',
|
||||||
|
'fc_care_modes_label' => 'Modes de garde',
|
||||||
|
'fc_add_mode_placeholder' => 'Ex: Nounou, Crèche...',
|
||||||
|
'fc_empty_kids_msg' => 'Aucun enfant n\'est configuré dans votre famille pour le moment.',
|
||||||
|
'fc_link_settings' => 'Aller aux paramètres généraux',
|
||||||
|
'fc_settings_updated' => 'Paramètres mis à jour avec succès !',
|
||||||
|
'fc_first_day' => 'Premier jour de la semaine',
|
||||||
|
'fc_helper_recap' => 'Récapitulatif Intervenant',
|
||||||
|
'fc_no_helper_leaves' => 'Aucun congé posé ce mois-ci.',
|
||||||
|
'fc_col_date' => 'Date',
|
||||||
|
'fc_col_type' => 'Type',
|
||||||
|
'fc_col_duration' => 'Durée',
|
||||||
|
'fc_full_year' => 'Année complète',
|
||||||
|
'fc_custom_period' => 'Période spécifique...',
|
||||||
|
'fc_worked_days' => 'Travaillés (Est.)',
|
||||||
|
'fc_tab_display' => '🖥️ Affichage',
|
||||||
|
'fc_display_title' => 'Paramètres d\'affichage',
|
||||||
|
'fc_display_desc' => 'Personnalisez l\'apparence et le comportement par défaut de votre calendrier.',
|
||||||
|
'fc_default_view' => 'Vue par défaut',
|
||||||
|
'fc_working_hours' => 'Horaires de garde (Défaut)',
|
||||||
|
'fc_working_hours_desc' => 'Exemple : 08:30-18:00',
|
||||||
|
'fc_error' => 'Erreur : ',
|
||||||
|
'fc_leave_catalog_title' => 'Catalogue des congés',
|
||||||
|
'fc_leave_catalog_desc' => 'Définissez les types de congés possibles pour toute la famille.',
|
||||||
|
'fc_add_leave_type' => 'Ajouter un type de congé',
|
||||||
|
'fc_col_code' => 'Code',
|
||||||
|
'fc_code_irreversible' => 'Irréversible',
|
||||||
|
'fc_col_label' => 'Label',
|
||||||
|
'fc_leave_label_ph' => 'Ex: RTT, Récup...',
|
||||||
|
'fc_quota_default' => 'Quota par défaut',
|
||||||
|
'fc_reset_month' => 'Mois de renouv.',
|
||||||
|
'fc_leave_obsolete' => 'Obsolète',
|
||||||
|
'fc_leave_code_placeholder' => 'Code...',
|
||||||
|
'fc_leave_method_fixed' => 'Fixe',
|
||||||
|
'fc_leave_method_accumulated'=> 'Cumul',
|
||||||
|
'fc_date_format_placeholder'=> 'JJ/MM',
|
||||||
|
'fc_date_format_title' => 'Format JJ/MM',
|
||||||
|
'fc_table_code' => 'Code',
|
||||||
|
'fc_table_method' => 'Méthode',
|
||||||
|
'fc_table_quota' => 'Quota',
|
||||||
|
'fc_table_renewal' => 'Renouv.',
|
||||||
|
'fc_label_carry_over' => 'Dont jours reportés (Périssables) ⚠️',
|
||||||
|
'fc_carry_over_note' => 'Partie du solde global qui expirera à la date butoir définie.',
|
||||||
|
'fc_carry_max' => 'Max Reportable',
|
||||||
|
'fc_carry_deadline' => 'Mois Expiration',
|
||||||
|
'fc_deadline_none' => 'Aucune',
|
||||||
|
'fc_allowance_help' => 'Fixe: Quota annuel. Graduel: Ajout par mois.',
|
||||||
|
'fc_carry_max_short' => 'Report max',
|
||||||
|
'fc_edit_leave' => '✏️ Modifier :',
|
||||||
|
'fc_not_modifiable' => 'Non modifiable',
|
||||||
|
'fc_add_leave' => '+ Ajouter',
|
||||||
|
'fc_err_code_label' => 'Le code et le libellé sont obligatoires.',
|
||||||
|
'fc_catalog_updated' => 'Catalogue mis à jour !',
|
||||||
|
'fc_balance_corrected' => 'Solde corrigé !',
|
||||||
|
'fc_no_leave_configured' => 'Aucun congé configuré.',
|
||||||
|
'fc_confirm_del_catalog' => 'Supprimer définitivement le congé %s du catalogue ? Cela le retirera également des membres qui l\'utilisent.',
|
||||||
|
'fc_confirm_del_member_leave'=> 'Voulez-vous vraiment retirer ce congé pour ce membre ?',
|
||||||
|
|
||||||
|
// Légendes Calendrier
|
||||||
'leg_presence' => 'Présence Pep',
|
'leg_presence' => 'Présence Pep',
|
||||||
'leg_school_holidays' => 'Vacances Scolaires',
|
'leg_school_holidays' => 'Vacances Scolaires',
|
||||||
'leg_public_holiday' => 'Férié',
|
'leg_public_holiday' => 'Férié',
|
||||||
@@ -207,7 +365,10 @@ return [
|
|||||||
'leg_extra_off' => 'Extra Off',
|
'leg_extra_off' => 'Extra Off',
|
||||||
'leg_centre' => 'Centre',
|
'leg_centre' => 'Centre',
|
||||||
'leg_avis' => 'Avis',
|
'leg_avis' => 'Avis',
|
||||||
'leg_pep_sick' => 'Pep Malade',
|
'fc_menu_sick' => 'Malade',
|
||||||
|
'care_nounou' => 'Nounou',
|
||||||
|
'care_centre' => 'Centre de loisirs',
|
||||||
|
'care_avis' => 'Avis',
|
||||||
|
|
||||||
'vac_toussaint' => 'Vacances de la Toussaint',
|
'vac_toussaint' => 'Vacances de la Toussaint',
|
||||||
'vac_noel' => 'Vacances de Noël',
|
'vac_noel' => 'Vacances de Noël',
|
||||||
@@ -217,12 +378,11 @@ return [
|
|||||||
'vac_ete' => "Vacances d'Été",
|
'vac_ete' => "Vacances d'Été",
|
||||||
'btn_off' => 'Off',
|
'btn_off' => 'Off',
|
||||||
'btn_extra' => 'Extra',
|
'btn_extra' => 'Extra',
|
||||||
|
'fc_alert_burn_days' => '⚠️ Risque de perte : %s j à poser avant la fin de cycle',
|
||||||
'fc_alert_burn_days' => '⚠️ Risque de perte : %s j à poser avant le %s',
|
'fc_alert_burn_jra' => '⚠️ Alerte : Jours reportés expirent à la fin de ce mois !',
|
||||||
'fc_alert_burn_jra' => '⚠️ JRA : Seuls 2j sont reportables après février !',
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// HOLIDAYS (VOYAGES)
|
// 6. VOYAGES (HOLIDAYS)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'hdl_title_list' => 'HouseHub - Mes Vacances',
|
'hdl_title_list' => 'HouseHub - Mes Vacances',
|
||||||
'hdl_title_detail' => 'HouseHub - Détail du voyage',
|
'hdl_title_detail' => 'HouseHub - Détail du voyage',
|
||||||
@@ -297,24 +457,23 @@ return [
|
|||||||
'hdl_to_place' => 'À placer',
|
'hdl_to_place' => 'À placer',
|
||||||
'hdl_from' => 'Du',
|
'hdl_from' => 'Du',
|
||||||
'hdl_to' => 'Au',
|
'hdl_to' => 'Au',
|
||||||
'hdl_btn_add_step' => 'Ajouter une étape',
|
|
||||||
'hdl_quick_edit_title' => 'Modification rapide',
|
|
||||||
'hdl_paid' => 'Payé',
|
|
||||||
'weather_forecast' => 'Prévisions météo',
|
|
||||||
'weather_not_available' => 'Météo indisponible',
|
|
||||||
'temp_max' => 'Temp. Max',
|
|
||||||
'weather_loading' => 'Chargement météo...',
|
|
||||||
'weather_error' => 'Météo indisponible',
|
|
||||||
'weather_sunny' => 'Ensoleillé',
|
|
||||||
'weather_cloudy' => 'Nuageux',
|
|
||||||
'weather_rainy' => 'Pluvieux',
|
|
||||||
'weather_snowy' => 'Neigeux',
|
|
||||||
'weather_historical' => 'Basé sur l\'historique (estimation)',
|
|
||||||
'err_invalid_holiday_id' => 'ID de voyage invalide.',
|
|
||||||
'hdl_default_exp_name' => 'Dépense',
|
|
||||||
'hdl_select_beneficiary' => '-- Sélectionner --',
|
|
||||||
|
|
||||||
// Anciennes clés Holidays non préfixées (conservées pour compatibilité si utilisées)
|
// Météo et spécifiques Holidays
|
||||||
|
'weather_forecast' => 'Prévisions météo',
|
||||||
|
'weather_not_available' => 'Météo indisponible',
|
||||||
|
'temp_max' => 'Temp. Max',
|
||||||
|
'weather_loading' => 'Chargement météo...',
|
||||||
|
'weather_error' => 'Météo indisponible',
|
||||||
|
'weather_sunny' => 'Ensoleillé',
|
||||||
|
'weather_cloudy' => 'Nuageux',
|
||||||
|
'weather_rainy' => 'Pluvieux',
|
||||||
|
'weather_snowy' => 'Neigeux',
|
||||||
|
'weather_historical' => 'Basé sur l\'historique (estimation)',
|
||||||
|
'err_invalid_holiday_id' => 'ID de voyage invalide.',
|
||||||
|
'hdl_default_exp_name' => 'Dépense',
|
||||||
|
'hdl_select_beneficiary' => '-- Sélectionner --',
|
||||||
|
|
||||||
|
// Anciennes clés (conservées au cas où)
|
||||||
'outbound_trip' => 'Aller',
|
'outbound_trip' => 'Aller',
|
||||||
'return_trip' => 'Retour',
|
'return_trip' => 'Retour',
|
||||||
'period' => 'Période',
|
'period' => 'Période',
|
||||||
@@ -367,7 +526,7 @@ return [
|
|||||||
'to_place' => '📥 À Placer',
|
'to_place' => '📥 À Placer',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// BUDGET (GLOBAL & CATEGORIES)
|
// 7. BUDGET (GLOBAL, SUIVI, PREV, EPARGNE, PROVISIONS)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'budget_page_title' => 'HouseHub - Budget',
|
'budget_page_title' => 'HouseHub - Budget',
|
||||||
'budget_main_header' => 'Gestion du Budget',
|
'budget_main_header' => 'Gestion du Budget',
|
||||||
@@ -375,9 +534,16 @@ return [
|
|||||||
'budget_tab_prev' => 'Budget 2026',
|
'budget_tab_prev' => 'Budget 2026',
|
||||||
'budget_tab_savings' => 'Épargne',
|
'budget_tab_savings' => 'Épargne',
|
||||||
'budget_tab_recap' => 'Récapitulatif',
|
'budget_tab_recap' => 'Récapitulatif',
|
||||||
|
'budget_tab_provisions' => 'Provisions',
|
||||||
|
'budget_tab_kids' => 'Enfants',
|
||||||
'budget_err_file_not_found' => 'Oups ! Fichier introuvable.',
|
'budget_err_file_not_found' => 'Oups ! Fichier introuvable.',
|
||||||
'budget_err_file_detail' => 'Vérifiez que le fichier views/%s.php existe bien.',
|
'budget_err_file_detail' => 'Vérifiez que le fichier views/%s.php existe bien.',
|
||||||
'budget_err_not_authorized' => 'Page non autorisée.',
|
'budget_err_not_authorized' => 'Page non autorisée.',
|
||||||
|
'bud_err_tech' => 'Une erreur technique est survenue.',
|
||||||
|
'bud_err_server' => 'Erreur serveur : ',
|
||||||
|
'bud_err_delete' => 'Erreur lors de la suppression.',
|
||||||
|
|
||||||
|
// Catégories (Budget)
|
||||||
'cat_income' => 'Revenus',
|
'cat_income' => 'Revenus',
|
||||||
'cat_fmcg' => 'Courses (FMCG)',
|
'cat_fmcg' => 'Courses (FMCG)',
|
||||||
'cat_fuel' => 'Essence',
|
'cat_fuel' => 'Essence',
|
||||||
@@ -386,7 +552,104 @@ return [
|
|||||||
'cat_others' => 'Autres / Imprévus',
|
'cat_others' => 'Autres / Imprévus',
|
||||||
'cat_savings' => 'Épargne',
|
'cat_savings' => 'Épargne',
|
||||||
|
|
||||||
// --- BUDGET : SUIVI ---
|
// Paramètres (Budget Settings)
|
||||||
|
'budget_settings_title' => 'Paramètres du Budget',
|
||||||
|
'bs_tab_accounts' => '💳 Comptes',
|
||||||
|
'bs_tab_categories' => '🏷️ Catégories',
|
||||||
|
'bs_tab_rules' => '🤖 Règles d\'import',
|
||||||
|
'bs_tab_salaries' => '💼 Salaires',
|
||||||
|
'bs_tab_csv' => '📥 Import CSV',
|
||||||
|
'bs_empty_accounts' => 'Aucun compte configuré.',
|
||||||
|
'bs_type_checking' => 'Courant',
|
||||||
|
'bs_type_savings' => 'Épargne',
|
||||||
|
'bs_badge_default' => 'Défaut',
|
||||||
|
'bs_owner' => 'Propriétaire',
|
||||||
|
'bs_shared_account' => 'Compte Commun',
|
||||||
|
'bs_empty_categories' => 'Aucune catégorie configurée.',
|
||||||
|
'bs_type_income' => 'Revenu',
|
||||||
|
'bs_type_expense' => 'Dépense',
|
||||||
|
'bs_type_savings_cat' => 'Épargne',
|
||||||
|
'bs_empty_rules' => 'Aucune règle d\'importation.',
|
||||||
|
'bs_empty_salaries' => 'Aucun salaire défini pour l\'année',
|
||||||
|
'bs_salaries_year_desc' => 'Configurations pour l\'année',
|
||||||
|
'bs_salary_net' => 'Salaire net',
|
||||||
|
'bs_salary_fees' => 'Mensualité / Frais',
|
||||||
|
'bs_add_account_title' => '+ Ajouter un compte',
|
||||||
|
'bs_account_name_ph' => 'Nom (ex: Livret A Bébé)',
|
||||||
|
'bs_confirm_delete' => 'Voulez-vous vraiment supprimer ce compte ?',
|
||||||
|
'bs_add_category_title' => '+ Ajouter une catégorie',
|
||||||
|
'bs_cat_code_ph' => 'Code (ex: FOOD)',
|
||||||
|
'bs_cat_label_ph' => 'Libellé (ex: Courses)',
|
||||||
|
'bs_cat_icon_ph' => 'Icône (ex: 🛒)',
|
||||||
|
'bs_confirm_delete_cat' => 'Voulez-vous vraiment supprimer cette catégorie ?',
|
||||||
|
'bs_add_rule_title' => 'Ajouter une règle',
|
||||||
|
'bs_rule_keyword_ph' => 'Mot-clé (ex: AUCHAN)',
|
||||||
|
'bs_confirm_delete_rule'=> 'Voulez-vous vraiment supprimer cette règle ?',
|
||||||
|
'bs_currency_label' => 'Devise du Foyer',
|
||||||
|
'bs_currency_desc' => 'Devise principale utilisée pour l\'affichage de vos comptes et budgets.',
|
||||||
|
'bs_currency_updated' => 'Devise mise à jour avec succès !',
|
||||||
|
'bud_settings_dyn_title' => 'Estimations Dynamiques (Enfants)',
|
||||||
|
'bud_dyn_nanny_fixed' => 'Salaire fixe Assistante Maternelle (€)',
|
||||||
|
'bud_dyn_nanny_daily' => 'Frais d\'entretien journaliers (€)',
|
||||||
|
'bud_dyn_nanny_aid' => 'Aide de l\'État (CMG) (€)',
|
||||||
|
'bud_dyn_nanny_cesu_avg' => 'Moyenne CESU versée (€)',
|
||||||
|
'bud_dyn_school_meal' => 'Prix repas Cantine (€)',
|
||||||
|
'bud_dyn_school_aftercare' => 'Prix Accueil Périscolaire soir (€)',
|
||||||
|
'bud_dyn_school_fullday' => 'Prix Accueil Journée (Mercredi/Vacances) (€)',
|
||||||
|
'bud_dyn_pattern_title' => 'Semaine type d\'école',
|
||||||
|
'bud_audit_month' => 'Mois analysé : %02d/%d',
|
||||||
|
'bud_audit_nanny_abs' => 'Absences %s déduites : %dj',
|
||||||
|
'bud_audit_child' => '👦 %s :',
|
||||||
|
'bud_audit_school_fee' => 'Cantine (%dj) + Garderie (%dj) + Centre (%dj) = %.2f€',
|
||||||
|
'bud_audit_sick' => 'Jours maladie déduits : %dj',
|
||||||
|
'bud_audit_nanny_full' => '💼 %s : Fixe (%.2f€) + Frais (%dj x %.2f€ = %.2f€) - Aide (%.2f€) - CESU (%.2f€) = %.2f€',
|
||||||
|
|
||||||
|
// CSV Mapping (Budget)
|
||||||
|
'bs_csv_title' => 'Format de l\'import CSV',
|
||||||
|
'bs_csv_desc' => 'Configurez ici les colonnes correspondantes à l\'export de votre banque.',
|
||||||
|
'bs_csv_delimiter' => 'Séparateur',
|
||||||
|
'bs_csv_delim_semi' => 'Point-virgule (;)',
|
||||||
|
'bs_csv_delim_comma' => 'Virgule (,)',
|
||||||
|
'bs_csv_delim_tab' => 'Tabulation',
|
||||||
|
'bs_csv_date_format' => 'Format de la Date',
|
||||||
|
'bs_csv_col_index' => 'Index des colonnes',
|
||||||
|
'bs_csv_col_index_help' => '(Attention : la 1ère colonne est la Numéro 0)',
|
||||||
|
'bs_csv_col_date' => 'N° Col : Date',
|
||||||
|
'bs_csv_col_label' => 'N° Col : Libellé / Motif',
|
||||||
|
'bs_csv_amount_mgmt' => 'Gestion des montants',
|
||||||
|
'bs_csv_amount_single' => 'Une seule colonne (Positif = Revenu, Négatif = Dépense)',
|
||||||
|
'bs_csv_amount_split' => 'Deux colonnes séparées (Débit / Crédit)',
|
||||||
|
'bs_csv_col_amount' => 'N° Col : Montant',
|
||||||
|
'bs_csv_col_debit' => 'N° Col : Débit',
|
||||||
|
'bs_csv_col_credit' => 'N° Col : Crédit',
|
||||||
|
'bs_csv_col_ref' => 'N° Col : Référence unique (Optionnel)',
|
||||||
|
'btn_save_format' => 'Enregistrer le format',
|
||||||
|
'bs_csv_saved' => 'Format CSV enregistré',
|
||||||
|
'bs_csv_settings_title' => 'Configuration du format',
|
||||||
|
'bs_csv_drop_title' => 'Glissez votre fichier CSV ici',
|
||||||
|
'bs_csv_drop_desc' => 'ou cliquez pour parcourir vos fichiers',
|
||||||
|
'bs_csv_err_type' => 'Erreur : Seuls les fichiers .csv sont autorisés.',
|
||||||
|
'bs_csv_file_ok' => 'Fichier chargé',
|
||||||
|
'bs_csv_preview_title' => 'Aperçu de vos colonnes',
|
||||||
|
'bs_csv_empty_col' => 'Vide',
|
||||||
|
'bs_csv_no_data' => 'Aucune ligne de transaction trouvée dans ce fichier.',
|
||||||
|
|
||||||
|
// Onboarding Budget
|
||||||
|
'bs_onb_title' => 'Bienvenue ! Configurons votre budget :',
|
||||||
|
'bs_onb_desc' => 'Complétez ces étapes pour que l\'application puisse calculer votre Reste à Vivre :',
|
||||||
|
'bs_onb_cat_link' => 'Catégories',
|
||||||
|
'bs_onb_cat_desc' => 'Créez au moins une catégorie de dépenses.',
|
||||||
|
'bs_onb_acc_link' => 'Comptes Bancaires',
|
||||||
|
'bs_onb_acc_desc' => 'Ajoutez au moins un compte courant ou épargne.',
|
||||||
|
'bs_onb_sal_link' => 'Salaires',
|
||||||
|
'bs_onb_sal_desc' => 'Renseignez les rentrées d\'argent de l\'année en cours.',
|
||||||
|
'bs_onb_items_link' => 'Lignes de Budget',
|
||||||
|
'bs_onb_items_desc' => 'Rendez-vous dans l\'onglet Récapitulatif pour lister vos charges fixes (Loyer, Assurances...).',
|
||||||
|
'bs_rule_search_ph' => '🔍 Rechercher un mot-clé (ex: AUCHAN)...',
|
||||||
|
'error_loading_settings' => 'Erreur lors du chargement des paramètres',
|
||||||
|
'bud_err_delete_system_cat' => 'Impossible de supprimer une catégorie système essentielle.',
|
||||||
|
|
||||||
|
// Budget : Suivi
|
||||||
'bud_rem_school' => 'École (Reste estimé)',
|
'bud_rem_school' => 'École (Reste estimé)',
|
||||||
'bud_rem_fmcg' => 'Courses (Reste estimé)',
|
'bud_rem_fmcg' => 'Courses (Reste estimé)',
|
||||||
'bud_rem_fuel' => 'Essence (Reste estimé)',
|
'bud_rem_fuel' => 'Essence (Reste estimé)',
|
||||||
@@ -439,22 +702,12 @@ return [
|
|||||||
'bud_add_title' => 'Ajouter',
|
'bud_add_title' => 'Ajouter',
|
||||||
'bud_edit_title' => 'Modifier la transaction',
|
'bud_edit_title' => 'Modifier la transaction',
|
||||||
'bud_to_define_js' => 'à définir',
|
'bud_to_define_js' => 'à définir',
|
||||||
'bud_bar_actual' => "Actuel",
|
'bud_bar_actual' => 'Actuel',
|
||||||
'bud_bar_theoretical' => "Théorique",
|
'bud_bar_theoretical' => 'Théorique',
|
||||||
'bud_bar_max_cap' => "Capacité Max",
|
'bud_bar_max_cap' => 'Capacité Max',
|
||||||
'bud_import_csv' => "Importer CSV",
|
'bud_select_beneficiary' => 'Choisir un revenu attendu',
|
||||||
'bud_csv_file' => "Sélectionnez le fichier de votre banque (.csv)",
|
|
||||||
'bud_preview' => "Prévisualiser",
|
|
||||||
'bud_validate_import' => "Veuillez vérifier et catégoriser les lignes avant validation.",
|
|
||||||
'bud_assign_month' => "Mois de gestion",
|
|
||||||
'bud_op_date' => "Date",
|
|
||||||
'bud_amount' => "Montant",
|
|
||||||
'bud_ignore' => "Ignorer (Crédit)",
|
|
||||||
'bud_to_define' => "À définir",
|
|
||||||
'bud_is_charge' => "Est une charge fixe...",
|
|
||||||
'bud_is_income' => "Est un revenu...",
|
|
||||||
|
|
||||||
// --- BUDGET : RECAP ---
|
// Budget : Récapitulatif
|
||||||
'bud_recap_monthly_title' => 'Récapitulatif Mensuel',
|
'bud_recap_monthly_title' => 'Récapitulatif Mensuel',
|
||||||
'bud_recap_btn_add' => 'Ajouter un frais / revenu',
|
'bud_recap_btn_add' => 'Ajouter un frais / revenu',
|
||||||
'bud_planned_amount' => 'Montant Prévu',
|
'bud_planned_amount' => 'Montant Prévu',
|
||||||
@@ -494,39 +747,38 @@ return [
|
|||||||
'bud_label_regularization' => 'Régularisation',
|
'bud_label_regularization' => 'Régularisation',
|
||||||
'bud_reg_none' => 'Aucune',
|
'bud_reg_none' => 'Aucune',
|
||||||
'bud_recap_confirm_delete' => 'Voulez-vous vraiment supprimer cet élément ?',
|
'bud_recap_confirm_delete' => 'Voulez-vous vraiment supprimer cet élément ?',
|
||||||
'bud_err_delete' => 'Erreur lors de la suppression.',
|
|
||||||
|
|
||||||
// --- BUDGET : EPARGNE ---
|
// Budget : Épargne
|
||||||
'bud_sav_add_month' => 'Saisir un mois',
|
'bud_sav_add_month' => 'Ajouter un mois',
|
||||||
'bud_sav_add_one_month' => '+1 Mois',
|
'bud_sav_add_one_month' => '+1 Mois',
|
||||||
'bud_sav_no_data' => 'Aucune donnée pour %s.',
|
'bud_sav_no_data' => 'Aucune donnée d\'épargne pour %s.',
|
||||||
'bud_sav_post_month' => 'Poste / Mois',
|
'bud_sav_post_month' => 'Poste / Mois',
|
||||||
'bud_sav_from_date' => 'Dès le %s',
|
'bud_sav_from_date' => 'Dès le %s',
|
||||||
'bud_sav_edit_modal' => 'Modifier avec Modale',
|
'bud_sav_edit_modal' => 'Éditer ce mois',
|
||||||
'bud_sav_delete_month' => 'Supprimer tout le mois',
|
'bud_sav_delete_month' => 'Supprimer ce mois',
|
||||||
'bud_sav_total_bank' => 'Total Banque',
|
'bud_sav_total_bank' => 'TOTAL BANQUE',
|
||||||
'bud_sav_extra' => 'Extra',
|
'bud_sav_extra' => 'NON ALLOUÉ (EXTRAS)',
|
||||||
'bud_sav_modal_title_add' => 'Saisir un mois',
|
'bud_sav_modal_title_add' => 'Ajouter un mois',
|
||||||
'bud_sav_modal_title_edit' => 'Modifier :',
|
'bud_sav_modal_title_edit' => 'Éditer',
|
||||||
'bud_sav_month_concerned' => 'Mois concerné',
|
'bud_sav_month_concerned' => 'Mois concerné',
|
||||||
'bud_sav_total_bank_eur' => 'Total en Banque (€)',
|
'bud_sav_total_bank_eur' => 'Total en Banque (€)',
|
||||||
'bud_sav_ventilation' => 'Ventilation',
|
'bud_sav_ventilation' => 'Ventilation des comptes',
|
||||||
'bud_sav_adj_help' => 'Utilisez l\'ajustement (+/-) pour recalculer automatiquement.',
|
'bud_sav_adj_help' => 'Saisissez les ajustements (+/-) pour recalculer automatiquement.',
|
||||||
'bud_sav_add_line' => 'Ligne',
|
'bud_sav_add_line' => 'Ajouter une ligne',
|
||||||
'bud_sav_current' => 'Actuel',
|
'bud_sav_current' => 'Actuel',
|
||||||
'bud_sav_adjust' => 'Ajust (+/-)',
|
'bud_sav_adjust' => 'Ajustement',
|
||||||
'bud_sav_new' => 'Nouveau',
|
'bud_sav_new' => 'Nouveau',
|
||||||
'bud_sav_sum_mode_title' => 'Activer l\'addition rapide',
|
'bud_sav_sum_mode_title' => 'Activer/Désactiver la calculatrice',
|
||||||
'bud_sav_selection' => 'Sélection :',
|
'bud_sav_selection' => 'Sélection :',
|
||||||
'bud_sav_ph_name' => 'Nom (ex: Vacances)',
|
'bud_sav_ph_name' => 'Nom du compte',
|
||||||
'bud_sav_saving' => 'Enregistrement...',
|
'bud_sav_new_line' => 'Nouvelle ligne',
|
||||||
'bud_err_tech' => 'Une erreur technique est survenue.',
|
'bud_sav_prompt_new_line' => 'Nom de la nouvelle ligne d\'épargne :',
|
||||||
'bud_sav_confirm_delete_month' => 'Supprimer TOUT le mois de %m pour %o ?',
|
'bud_sav_confirm_delete_month' => 'Voulez-vous vraiment supprimer toutes les données de %m pour %o ?',
|
||||||
'bud_sav_prompt_duplicate' => "Voulez-vous copier les données de %s vers %t1 ?\n\nSaisissez le nouveau TOTAL en banque (€) pour %t2 :",
|
'bud_sav_prompt_duplicate' => "Voulez-vous dupliquer les données de %s vers %t1 ?\n\nSaisissez le nouveau TOTAL en banque (€) pour %t2 :",
|
||||||
'bud_err_server' => 'Erreur serveur : ',
|
|
||||||
'bud_err_network_dup' => 'Erreur réseau lors de la duplication.',
|
'bud_err_network_dup' => 'Erreur réseau lors de la duplication.',
|
||||||
|
'bud_sav_saving' => 'Sauvegarde en cours...',
|
||||||
|
|
||||||
// --- BUDGET : PREVISIONNEL ---
|
// Budget : Prévisionnel
|
||||||
'bud_prev_incomes' => 'Revenus',
|
'bud_prev_incomes' => 'Revenus',
|
||||||
'bud_prev_person' => 'Personne',
|
'bud_prev_person' => 'Personne',
|
||||||
'bud_prev_salary' => 'Salaire',
|
'bud_prev_salary' => 'Salaire',
|
||||||
@@ -560,57 +812,94 @@ return [
|
|||||||
'bud_prev_err_no_history' => 'Impossible : pas d\'historique disponible pour copier.',
|
'bud_prev_err_no_history' => 'Impossible : pas d\'historique disponible pour copier.',
|
||||||
'bud_prev_confirm_copy' => "Voulez-vous copier les données de %s vers %t ?\n\n⚠️ Cela écrasera toutes les valeurs déjà présentes pour %t.",
|
'bud_prev_confirm_copy' => "Voulez-vous copier les données de %s vers %t ?\n\n⚠️ Cela écrasera toutes les valeurs déjà présentes pour %t.",
|
||||||
'bud_prev_confirm_transfers' => "Confirmer que %p a bien effectué tous ses virements pour %m ?\n\nCela mettra à jour l'Épargne automatiquement.",
|
'bud_prev_confirm_transfers' => "Confirmer que %p a bien effectué tous ses virements pour %m ?\n\nCela mettra à jour l'Épargne automatiquement.",
|
||||||
'bud_sav_modal_title_add' => 'Saisir un mois',
|
'bud_prev_label_name' => 'Nom',
|
||||||
'bud_sav_ph_name' => 'Nom (ex: Vacances)',
|
'bud_prev_target_ph' => 'Ex: 150',
|
||||||
'bud_prev_label_name' => 'Nom',
|
'bud_prev_transfer_to' => 'Vers',
|
||||||
|
|
||||||
|
// Budget : Tricount / Avances
|
||||||
|
'bud_adv_title' => 'Avances & Tricount Familial',
|
||||||
|
'bud_adv_add' => 'Ajouter une avance',
|
||||||
|
'bud_adv_edit' => 'Modifier l\'avance',
|
||||||
|
'bud_adv_who_paid' => 'Qui a payé ?',
|
||||||
|
'bud_adv_payer' => 'Payeur',
|
||||||
|
'bud_adv_has_advanced' => '%s a avancé :',
|
||||||
|
'bud_adv_cc_label' => 'sur le compte commun',
|
||||||
|
'bud_adv_livret_label' => 'depuis son livret d\'épargne',
|
||||||
|
'bud_adv_cc_balance_title' => 'Équilibre du Compte Commun',
|
||||||
|
'bud_adv_owed_to' => '<strong>%s €</strong> sont dus à <strong>%s</strong>',
|
||||||
|
'bud_adv_balanced' => 'Les comptes sont parfaitement équilibrés !',
|
||||||
|
'bud_adv_saved_badge' => 'Épargne',
|
||||||
|
'bud_adv_already_saved' => 'Cet argent provient d\'un livret d\'épargne',
|
||||||
|
'bud_adv_ph_desc' => 'Ex: Courses, Facture Électricité...',
|
||||||
|
'bud_adv_confirm_resolve' => 'Confirmer le remboursement complet de cette avance ?',
|
||||||
|
'bud_adv_confirm_delete' => 'Supprimer définitivement cette avance ?',
|
||||||
|
|
||||||
|
// Budget : Provisions & Optimisation
|
||||||
|
'budget_provisions_title' => 'Provisions & Optimisation de Trésorerie',
|
||||||
|
'btn_optimize_cashflow' => 'Optimiser mes Économies',
|
||||||
|
'add_new_provision' => 'Anticiper une grosse dépense à venir',
|
||||||
|
'provision_label' => 'Libellé de la dépense',
|
||||||
|
'provision_placeholder_wood' => 'Ex: Commande bois de chauffage, Révision voiture...',
|
||||||
|
'amount' => 'Montant',
|
||||||
|
'expected_date' => 'Date prévue',
|
||||||
|
'upcoming_provisions_list' => 'Dépenses prévues au calendrier',
|
||||||
|
'optimization_assistant_title' => 'Assistant d\'Optimisation des Quinzaines',
|
||||||
|
'success_add_provision' => 'Dépense prévisionnelle ajoutée !',
|
||||||
|
'no_provisions' => 'Aucune dépense prévisionnelle enregistrée pour le moment.',
|
||||||
|
'savings_this_month' => 'Économies globales à placer ce mois-ci',
|
||||||
|
'calculate' => 'Calculer la stratégie optimale',
|
||||||
|
'date_past_warning' => 'La date sélectionnée est déjà passée !',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// CADEAUX (GIFTS)
|
// 8. CADEAUX (GIFTS)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'gift_page_title' => 'HouseHub - Liste de cadeaux',
|
'gift_page_title' => 'Liste des Cadeaux',
|
||||||
'gift_occ_tio' => 'Tió',
|
'gift_main_title' => 'Cadeaux de %s',
|
||||||
'gift_occ_noel' => 'Noël',
|
'gift_filter_all_children' => 'Tous les enfants',
|
||||||
'gift_occ_rois' => 'Rois mages',
|
'gift_filter_all_adults' => 'Tous les adultes',
|
||||||
'gift_occ_anniv' => 'Anniversaire',
|
'gift_add_gift' => 'Ajouter',
|
||||||
'gift_occ_sant' => 'Saint',
|
'gift_empty_state_no_gifts' => 'Aucun cadeau enregistré pour cette fête.',
|
||||||
'gift_main_title' => 'Liste de cadeaux %s',
|
'gift_empty_state_no_filter' => 'Aucun cadeau ne correspond à vos filtres.',
|
||||||
'gift_aria_change_view' => 'Changer la vue',
|
'gift_paid_by' => 'Payé par %s',
|
||||||
'gift_view_nadal' => 'Noël',
|
'gift_liquidations' => 'Bilan & Remboursements',
|
||||||
'gift_view_anniv' => 'Anniversaires',
|
'gift_no_debt' => 'Tout est à jour, aucune dette !',
|
||||||
'gift_view_by_party' => 'Vue par fête',
|
'gift_owes' => 'doit',
|
||||||
'gift_no_gifts' => 'Aucun cadeau enregistré pour %s dans cette vue.',
|
'gift_view_matrix' => 'Voir la matrice détaillée',
|
||||||
'gift_add_gift' => 'Ajouter un cadeau',
|
'gift_debtor' => 'Débiteur',
|
||||||
'gift_paid_by' => 'payé par %s',
|
'gift_creditor' => 'Créancier',
|
||||||
'gift_summary_title' => 'Résumé du budget',
|
'gift_col_adult' => 'Pour le compte de',
|
||||||
'gift_col_adult' => 'Adulte',
|
'gift_modal_payer' => 'Payé par',
|
||||||
'gift_col_child' => 'Enfant',
|
'gift_modal_gift_name' => 'Description du cadeau',
|
||||||
'gift_col_party' => 'Fête',
|
'gift_modal_price' => 'Prix estimé / payé (€)',
|
||||||
'gift_debtor' => 'Débiteur',
|
'gift_modal_link' => 'Lien du produit (URL)',
|
||||||
'gift_creditor' => 'Créancier',
|
'gift_modal_title_add' => 'Ajouter pour %s',
|
||||||
'gift_liquidations' => 'Tricount',
|
'gift_modal_title_edit' => 'Modifier le cadeau',
|
||||||
'gift_no_debt' => 'Aucune dette en cours.',
|
'gift_confirm_delete' => 'Voulez-vous vraiment supprimer ce cadeau ?',
|
||||||
'gift_owes' => 'doit',
|
'gift_settings_title' => 'Configuration des Fêtes',
|
||||||
'gift_to' => 'à',
|
'gift_settings_add_title' => '+ Ajouter une occasion',
|
||||||
'gift_detailed_list' => 'Liste détaillée des cadeaux',
|
'gift_settings_name_placeholder' => 'Nom (ex: Saint Valentin)',
|
||||||
'gift_col_gift' => 'Cadeau',
|
'gift_settings_date_placeholder' => 'MM-JJ',
|
||||||
'gift_col_link' => 'Lien',
|
'gift_settings_date_help' => 'La date (MM-JJ) est optionnelle. Laissez vide pour une fête mobile.',
|
||||||
'gift_modal_title_add' => 'Ajouter un cadeau pour %s',
|
'gift_settings_empty' => 'Aucune fête configurée.',
|
||||||
'gift_modal_title_edit' => 'Modifier le cadeau',
|
'gift_settings_active' => 'Actif',
|
||||||
'gift_modal_payer' => 'Payé par',
|
'gift_settings' => 'Configuration',
|
||||||
'gift_modal_gift_name' => 'Nom du cadeau',
|
'gift_tab_occasions' => 'Fêtes & Occasions',
|
||||||
'gift_modal_ph_name' => 'ex: Lego Star Wars',
|
'gift_tab_proches' => 'Proches (Hors foyer)',
|
||||||
'gift_modal_price' => 'Prix (€)',
|
'gift_settings_icon' => 'Icône',
|
||||||
'gift_modal_link' => 'Lien (optionnel)',
|
'gift_settings_date' => 'Date (MM-JJ)',
|
||||||
'gift_confirm_delete' => 'Voulez-vous vraiment supprimer ce cadeau ?',
|
'gift_settings_add_occ' => 'Ajouter une fête',
|
||||||
'gift_filter_all_children' => 'Tous les enfants',
|
'gift_settings_add_proche' => 'Ajouter un proche',
|
||||||
'gift_filter_all_adults' => 'Tous les adultes',
|
'gift_role_proche_adulte' => 'Adulte (Oncle, ami...)',
|
||||||
'gift_empty_state_no_gifts' => 'Aucun cadeau pour le moment.',
|
'gift_role_proche_enfant' => 'Enfant (Neveu, nièce...)',
|
||||||
'gift_empty_state_no_filter' => 'Aucun cadeau ne correspond au filtre.',
|
'gift_ph_proche_name' => 'Prénom',
|
||||||
'gift_view_matrix' => 'Voir la matrice détaillée',
|
'gift_role_label' => 'Rôle',
|
||||||
|
'gift_settings_saved' => 'Paramètres enregistrés !',
|
||||||
|
'gift_settings_edit_occ' => 'Modifier la fête',
|
||||||
|
'gift_settings_edit_proche' => 'Modifier le proche',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// GARAGE MANAGER
|
// 9. GARAGE
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'menu_garage' => 'Garage',
|
|
||||||
'garage_page_title' => 'HouseHub - Garage',
|
'garage_page_title' => 'HouseHub - Garage',
|
||||||
'garage_stat_vehicles' => 'Véhicules',
|
'garage_stat_vehicles' => 'Véhicules',
|
||||||
'garage_stat_maintenances' => 'Entretiens',
|
'garage_stat_maintenances' => 'Entretiens',
|
||||||
@@ -640,7 +929,6 @@ return [
|
|||||||
'garage_photo' => 'Photo',
|
'garage_photo' => 'Photo',
|
||||||
'garage_notes' => 'Notes',
|
'garage_notes' => 'Notes',
|
||||||
'garage_maint_type' => 'Type d\'entretien',
|
'garage_maint_type' => 'Type d\'entretien',
|
||||||
'garage_date' => 'Date',
|
|
||||||
'garage_km_at' => 'Kilométrage au moment',
|
'garage_km_at' => 'Kilométrage au moment',
|
||||||
'garage_labor_cost' => 'Coût main d\'œuvre (€)',
|
'garage_labor_cost' => 'Coût main d\'œuvre (€)',
|
||||||
'garage_description' => 'Description',
|
'garage_description' => 'Description',
|
||||||
@@ -659,4 +947,79 @@ return [
|
|||||||
'garage_change_photo' => 'Changer la photo',
|
'garage_change_photo' => 'Changer la photo',
|
||||||
'garage_new_photo' => 'Nouvelle photo',
|
'garage_new_photo' => 'Nouvelle photo',
|
||||||
'garage_update_photo' => 'Mettre à jour',
|
'garage_update_photo' => 'Mettre à jour',
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 10. LISTES & COURSES
|
||||||
|
// ==========================================
|
||||||
|
'liste_placeholder_add' => 'Ajouter un article…',
|
||||||
|
'liste_btn_add' => 'Ajouter',
|
||||||
|
'liste_btn_uncheck_all' => 'Tout décocher',
|
||||||
|
'liste_btn_delete_picked' => 'Supprimer les cochés',
|
||||||
|
'liste_btn_clear_all' => 'Vider la liste',
|
||||||
|
'liste_subtitle' => '{pending} à faire · {cart} cochés',
|
||||||
|
'liste_added' => 'Ajouté',
|
||||||
|
'liste_updated' => 'Modifié',
|
||||||
|
'liste_deleted' => 'Supprimé',
|
||||||
|
'liste_already_in' => 'Déjà dans la liste',
|
||||||
|
'liste_confirm_clear' => 'Vider toute la liste ?',
|
||||||
|
'liste_confirm_delete_list' => 'Supprimer cette liste et tous ses articles ?',
|
||||||
|
'liste_history_title' => 'Récemment utilisés — clic pour rajouter',
|
||||||
|
'liste_new_list' => 'Nouvelle liste',
|
||||||
|
'liste_list_name_placeholder'=> 'Nom de la liste…',
|
||||||
|
'liste_create' => 'Créer',
|
||||||
|
'liste_rename_list' => 'Renommer',
|
||||||
|
'liste_new_name' => 'Nouveau nom :',
|
||||||
|
'liste_list_deleted' => 'Liste supprimée',
|
||||||
|
'liste_list_created' => 'Liste créée',
|
||||||
|
'liste_list_renamed' => 'Liste renommée',
|
||||||
|
'liste_edit_placeholder' => 'Modifier l\'article :',
|
||||||
|
'liste_empty_state' => 'Créez votre première liste avec le bouton + ci-dessus.',
|
||||||
|
'liste_settings_title' => 'Listes — historique',
|
||||||
|
'liste_settings_intro' => 'Nombre maximum d\'articles mémorisés dans l\'historique (commun à toutes les listes).',
|
||||||
|
'liste_settings_history_max' => 'Taille de l\'historique',
|
||||||
|
'liste_settings_history_hint'=> 'Entre 1 et 50.',
|
||||||
|
'groceries_settings_saved' => 'Préférences enregistrées.',
|
||||||
|
'groceries_settings_error' => 'Impossible d\'enregistrer :',
|
||||||
|
'groceries_err_duplicate' => 'Ce produit est déjà dans la liste.',
|
||||||
|
|
||||||
|
// --- MODULE MEALS ---
|
||||||
|
'menu_meals' => 'Repas',
|
||||||
|
'mod_meals_name' => 'Menus',
|
||||||
|
'mod_meals_desc' => 'Planifiez les repas de la semaine pour toute la famille.',
|
||||||
|
'meals_board_title' => 'Planning Repas',
|
||||||
|
'meals_copy_lunch' => 'Copier le midi entier',
|
||||||
|
'meals_copy_dinner' => 'Copier le soir entier',
|
||||||
|
'meal_lunch' => 'Midi',
|
||||||
|
'meal_dinner' => 'Soir',
|
||||||
|
'btn_apply_all' => 'Tous',
|
||||||
|
'btn_copy_to' => 'Copier vers...',
|
||||||
|
'modal_copy_title' => 'Copier vers...',
|
||||||
|
'source_individual' => 'Plat individuel',
|
||||||
|
'source_row' => 'Repas complet',
|
||||||
|
'week_current' => 'Semaine actuelle',
|
||||||
|
'week_next' => 'Semaine prochaine',
|
||||||
|
'meals_no_members' => 'Veuillez configurer les membres de votre famille dans les paramètres.',
|
||||||
|
'day_full_mon' => 'Lundi', 'day_full_tue' => 'Mardi', 'day_full_wed' => 'Mercredi',
|
||||||
|
'day_full_thu' => 'Jeudi', 'day_full_fri' => 'Vendredi', 'day_full_sat' => 'Samedi', 'day_full_sun' => 'Dimanche',
|
||||||
|
'meal_title' => 'Planificateur de Repas',
|
||||||
|
'meal_edit_title' => 'Modifier le repas',
|
||||||
|
'meal_label_name' => 'Menu prévu',
|
||||||
|
'meal_ph_name' => 'Ex: Lasagnes maison',
|
||||||
|
'meal_week' => 'Semaine',
|
||||||
|
'meal_empty_slot' => 'À définir...',
|
||||||
|
'meal_current_week' => 'Semaine en cours',
|
||||||
|
'meal_saved' => 'Repas enregistré !',
|
||||||
|
'meal_btn_import_pdf' => 'Importer Menu PDF',
|
||||||
|
'meal_import_title' => 'Importer un menu de cantine',
|
||||||
|
'meal_import_child' => 'Pour quel enfant ?',
|
||||||
|
'meal_select_child' => 'Sélectionner l\'enfant',
|
||||||
|
'meal_pdf_file' => 'Fichier PDF (Convivio)',
|
||||||
|
'meal_drop_pdf' => 'Cliquez pour ajouter le PDF',
|
||||||
|
'meal_import_success' => 'Menus importés avec succès !',
|
||||||
|
'mod_food_name' => 'Cuisine & Courses',
|
||||||
|
'food_tab_meals' => 'Menu de la semaine',
|
||||||
|
'food_tab_list' => 'Liste de courses',
|
||||||
|
'menu_food' => 'Food',
|
||||||
|
'mod_food_desc' => 'Gérez vos menus de la semaine et vos listes de courses partagées.',
|
||||||
|
'cta_food' => 'Mijoter',
|
||||||
];
|
];
|
||||||
+20
-4
@@ -1,9 +1,23 @@
|
|||||||
<?php
|
<?php
|
||||||
|
// includes/meta_db.php
|
||||||
// Connexion à la base meta (gestion des familles et utilisateurs)
|
// Connexion à la base meta (gestion des familles et utilisateurs)
|
||||||
$meta_host = getenv('DB_HOST') ?: 'househub-db';
|
|
||||||
$meta_db = 'househub_meta';
|
$envHost = getenv('DB_HOST');
|
||||||
$meta_user = getenv('DB_USER') ?: 'househub';
|
|
||||||
$meta_pass = getenv('DB_PASS') ?: 'changeme';
|
// Détection stricte : Docker injecte toujours 'househub-db' via le docker-compose
|
||||||
|
if ($envHost === 'househub-db') {
|
||||||
|
// Environnement Docker (Ton collègue ou la Prod)
|
||||||
|
$meta_host = 'househub-db';
|
||||||
|
$meta_user = getenv('DB_USER') ?: 'househub';
|
||||||
|
$meta_pass = getenv('DB_PASS') ?: 'changeme';
|
||||||
|
} else {
|
||||||
|
// Environnement Local XAMPP (Toi)
|
||||||
|
$meta_host = '127.0.0.1'; // Plus stable que 'localhost' sous XAMPP
|
||||||
|
$meta_user = 'root';
|
||||||
|
$meta_pass = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta_db = 'househub_meta';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$meta_pdo = new PDO(
|
$meta_pdo = new PDO(
|
||||||
@@ -14,8 +28,10 @@ try {
|
|||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
PDO::ATTR_EMULATE_PREPARES => false,
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_general_ci",
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} catch (\PDOException $e) {
|
} catch (\PDOException $e) {
|
||||||
die(json_encode(['error' => 'Meta DB unavailable: ' . $e->getMessage()]));
|
die(json_encode(['error' => 'Meta DB unavailable: ' . $e->getMessage()]));
|
||||||
}
|
}
|
||||||
|
?>
|
||||||
@@ -43,7 +43,7 @@ if ($_has_custom_bg): ?>
|
|||||||
<section class="pf-section">
|
<section class="pf-section">
|
||||||
<h2 style="color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,0.6);"><?= tr('home_modules_title') ?></h2>
|
<h2 style="color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,0.6);"><?= tr('home_modules_title') ?></h2>
|
||||||
|
|
||||||
<?php $mods = $_SESSION['enabled_modules'] ?? ['calendar','budget','holidays','gifts','garage','calendar_ios']; ?>
|
<?php $mods = $_SESSION['enabled_modules'] ?? ['calendar','budget','holidays','gifts','food','garage','calendar_ios']; ?>
|
||||||
<div class="pf-modules-grid">
|
<div class="pf-modules-grid">
|
||||||
|
|
||||||
<?php if (in_array('calendar', $mods)): ?>
|
<?php if (in_array('calendar', $mods)): ?>
|
||||||
@@ -91,6 +91,15 @@ if ($_has_custom_bg): ?>
|
|||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (in_array('food', $mods)): ?>
|
||||||
|
<a href="/food.php" class="pf-module-card">
|
||||||
|
<div class="pf-card-icon">🍳</div>
|
||||||
|
<h3 class="pf-card-title"><?= tr('mod_food_name') ?></h3>
|
||||||
|
<div class="pf-card-desc"><?= tr('mod_food_desc') ?></div>
|
||||||
|
<span class="pf-card-cta"><?= tr('cta_food') ?></span>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (in_array('memo', $mods)): ?>
|
<?php if (in_array('memo', $mods)): ?>
|
||||||
<a href="/memo.php" class="pf-module-card">
|
<a href="/memo.php" class="pf-module-card">
|
||||||
<div class="pf-card-icon">📝</div>
|
<div class="pf-card-icon">📝</div>
|
||||||
@@ -109,14 +118,6 @@ if ($_has_custom_bg): ?>
|
|||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (in_array('liste', $mods)): ?>
|
|
||||||
<a href="/liste.php" class="pf-module-card">
|
|
||||||
<div class="pf-card-icon">🛒</div>
|
|
||||||
<h3 class="pf-card-title"><?= tr('mod_liste_name') ?></h3>
|
|
||||||
<div class="pf-card-desc"><?= tr('mod_liste_desc') ?></div>
|
|
||||||
<span class="pf-card-cta"><?= tr('cta_cart') ?></span>
|
|
||||||
</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (in_array('calendar_ios', $mods)): ?>
|
<?php if (in_array('calendar_ios', $mods)): ?>
|
||||||
<a href="/calendar-ios.php" class="pf-module-card">
|
<a href="/calendar-ios.php" class="pf-module-card">
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
<?php
|
|
||||||
require __DIR__ . '/includes/auth.php';
|
|
||||||
require_login();
|
|
||||||
require_once __DIR__ . '/includes/i18n.php';
|
|
||||||
|
|
||||||
$pageTitle = tr('mod_liste_name') . ' — HouseHub';
|
|
||||||
$activePage = 'liste';
|
|
||||||
require __DIR__ . '/header.php';
|
|
||||||
?>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/modules/liste/assets/liste.css">
|
|
||||||
|
|
||||||
<main class="liste-main">
|
|
||||||
|
|
||||||
<!-- Tabs des listes -->
|
|
||||||
<div class="liste-tabs-bar">
|
|
||||||
<div class="liste-tabs" id="liste-tabs"></div>
|
|
||||||
<button class="liste-tab-add" id="btn-add-list" title="<?= htmlspecialchars(tr('liste_new_list')) ?>">+</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Formulaire de création de liste inline -->
|
|
||||||
<div class="liste-new-form hidden" id="liste-new-form">
|
|
||||||
<input type="text" id="new-list-name" maxlength="100"
|
|
||||||
placeholder="<?= htmlspecialchars(tr('liste_list_name_placeholder')) ?>" autocomplete="off">
|
|
||||||
<button id="btn-confirm-new-list"><?= htmlspecialchars(tr('liste_create')) ?></button>
|
|
||||||
<button id="btn-cancel-new-list"><?= htmlspecialchars(tr('cancel')) ?></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Corps de la liste active -->
|
|
||||||
<div class="liste-body" id="liste-body">
|
|
||||||
|
|
||||||
<div class="liste-add-card">
|
|
||||||
<input type="text" id="liste-input" maxlength="500"
|
|
||||||
placeholder="<?= htmlspecialchars(tr('liste_placeholder_add')) ?>" autocomplete="off">
|
|
||||||
<button id="btn-liste-add"><?= htmlspecialchars(tr('liste_btn_add')) ?></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="liste-toolbar" id="liste-toolbar">
|
|
||||||
<button class="btn-tool" id="btn-uncheck-all"><?= htmlspecialchars(tr('liste_btn_uncheck_all')) ?></button>
|
|
||||||
<button class="btn-tool btn-tool-danger" id="btn-delete-picked"><?= htmlspecialchars(tr('liste_btn_delete_picked')) ?></button>
|
|
||||||
<button class="btn-tool btn-tool-danger" id="btn-clear-all"><?= htmlspecialchars(tr('liste_btn_clear_all')) ?></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="liste-items-root"></div>
|
|
||||||
<div id="liste-history-root"></div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const LISTE_TRANSLATIONS = {
|
|
||||||
already_in: <?= json_encode(tr('liste_already_in')) ?>,
|
|
||||||
confirm_clear: <?= json_encode(tr('liste_confirm_clear')) ?>,
|
|
||||||
confirm_delete_list: <?= json_encode(tr('liste_confirm_delete_list')) ?>,
|
|
||||||
rename_list: <?= json_encode(tr('liste_rename_list')) ?>,
|
|
||||||
new_name: <?= json_encode(tr('liste_new_name')) ?>,
|
|
||||||
list_deleted: <?= json_encode(tr('liste_list_deleted')) ?>,
|
|
||||||
list_created: <?= json_encode(tr('liste_list_created')) ?>,
|
|
||||||
list_renamed: <?= json_encode(tr('liste_list_renamed')) ?>,
|
|
||||||
edit_placeholder: <?= json_encode(tr('liste_edit_placeholder')) ?>,
|
|
||||||
history_title: <?= json_encode(tr('liste_history_title')) ?>,
|
|
||||||
added: <?= json_encode(tr('liste_added')) ?>,
|
|
||||||
updated: <?= json_encode(tr('liste_updated')) ?>,
|
|
||||||
deleted: <?= json_encode(tr('liste_deleted')) ?>,
|
|
||||||
cancel: <?= json_encode(tr('cancel')) ?>,
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<script src="/modules/liste/assets/liste.js"></script>
|
|
||||||
<?php require __DIR__ . '/footer.php'; ?>
|
|
||||||
@@ -51,6 +51,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$_SESSION['family_db'] = $user['db_name'];
|
$_SESSION['family_db'] = $user['db_name'];
|
||||||
$_SESSION['app_lang'] = $user['lang'] ?? 'fr';
|
$_SESSION['app_lang'] = $user['lang'] ?? 'fr';
|
||||||
$_SESSION['enabled_modules'] = json_decode($user['enabled_modules'] ?? '["calendar","budget","holidays","gifts","calendar_ios"]', true);
|
$_SESSION['enabled_modules'] = json_decode($user['enabled_modules'] ?? '["calendar","budget","holidays","gifts","calendar_ios"]', true);
|
||||||
|
|
||||||
|
// --- NOUVEAU : Logique "Se souvenir de moi" ---
|
||||||
|
if (isset($_POST['remember_me']) && $_POST['remember_me'] === '1') {
|
||||||
|
$token = bin2hex(random_bytes(32)); // Token aléatoire sécurisé
|
||||||
|
$hashedToken = hash('sha256', $token); // On stocke le hash en BDD
|
||||||
|
|
||||||
|
$meta_pdo->prepare("UPDATE users SET remember_token = ? WHERE id = ?")->execute([$hashedToken, $user['id']]);
|
||||||
|
|
||||||
|
// Création du cookie sécurisé (Expire dans 30 jours, HTTPOnly)
|
||||||
|
$cookieValue = $user['id'] . ':' . $token;
|
||||||
|
setcookie('hh_remember', $cookieValue, time() + (86400 * 30), '/', '', isset($_SERVER['HTTPS']), true);
|
||||||
|
}
|
||||||
|
// ----------------------------------------------
|
||||||
|
|
||||||
$redirectTo = $_GET['redirect'] ?? '/index.php';
|
$redirectTo = $_GET['redirect'] ?? '/index.php';
|
||||||
header('Location: ' . $redirectTo);
|
header('Location: ' . $redirectTo);
|
||||||
exit;
|
exit;
|
||||||
@@ -95,6 +109,11 @@ require __DIR__ . '/header.php';
|
|||||||
required placeholder="••••••" autocomplete="current-password">
|
required placeholder="••••••" autocomplete="current-password">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="pf-form-group" style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<input type="checkbox" id="remember_me" name="remember_me" value="1" class="pf-checkbox-lg">
|
||||||
|
<label class="pf-label" for="remember_me" style="margin: 0; cursor: pointer;"><?= tr('login_remember_me') ?? 'Se souvenir de moi' ?></label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="pf-btn pf-btn-block">
|
<button type="submit" class="pf-btn pf-btn-block">
|
||||||
<?= tr('btn_login_submit') ?>
|
<?= tr('btn_login_submit') ?>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+15
-1
@@ -1,6 +1,20 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
|
// --- NOUVEAU : Révocation du "Se souvenir de moi" ---
|
||||||
|
if (isset($_SESSION['user']['id'])) {
|
||||||
|
require_once __DIR__ . '/includes/meta_db.php';
|
||||||
|
// On invalide le token en base de données
|
||||||
|
$stmt = $meta_pdo->prepare("UPDATE users SET remember_token = NULL WHERE id = ?");
|
||||||
|
$stmt->execute([$_SESSION['user']['id']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// On détruit le cookie côté navigateur
|
||||||
|
if (isset($_COOKIE['hh_remember'])) {
|
||||||
|
setcookie('hh_remember', '', time() - 3600, '/', '', isset($_SERVER['HTTPS']), true);
|
||||||
|
}
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
// On vide toutes les variables de session
|
// On vide toutes les variables de session
|
||||||
$_SESSION = [];
|
$_SESSION = [];
|
||||||
|
|
||||||
@@ -21,6 +35,6 @@ if (ini_get("session.use_cookies")) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// On renvoie vers la page de login (ou index si tu préfères)
|
// On renvoie vers la page de login
|
||||||
header('Location: /login.php');
|
header('Location: /login.php');
|
||||||
exit;
|
exit;
|
||||||
+66
-31
@@ -1,46 +1,81 @@
|
|||||||
<?php
|
<?php
|
||||||
// Script de migration pour mettre à jour TOUTES les bases de données familiales
|
// migrate.php
|
||||||
require_once __DIR__ . '/includes/meta_db.php';
|
require __DIR__ . '/includes/auth.php';
|
||||||
|
require __DIR__ . '/includes/meta_db.php';
|
||||||
|
require_login();
|
||||||
|
|
||||||
echo "<h1>🚀 Début de la migration...</h1>";
|
if (empty($_SESSION['user']['is_admin'])) {
|
||||||
|
die("Accès refusé. Droits administrateur requis.");
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Récupérer toutes les bases de données des familles
|
try {
|
||||||
$stmt = $meta_pdo->query("SELECT name, db_name FROM families WHERE db_name != ''");
|
$stmt = $meta_pdo->query("SELECT db_name FROM families");
|
||||||
$families = $stmt->fetchAll();
|
$families = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$host = getenv('DB_HOST') ?: 'househub-db';
|
foreach ($families as $fam) {
|
||||||
$user = getenv('DB_USER') ?: 'househub';
|
$dbName = $fam['db_name'];
|
||||||
$pass = getenv('DB_PASS') ?: 'changeme';
|
echo "Migration de la base : $dbName ... <br>";
|
||||||
|
|
||||||
// 2. Boucler sur chaque famille
|
// --- DÉBUT DU BLOC MODIFIÉ ---
|
||||||
foreach ($families as $f) {
|
$envHost = getenv('DB_HOST');
|
||||||
$db_name = $f['db_name'];
|
if ($envHost === 'househub-db') {
|
||||||
echo "Mise à jour de la famille <strong>{$f['name']}</strong> (Base : $db_name) ... ";
|
$host = 'househub-db';
|
||||||
|
$user = getenv('DB_USER') ?: 'househub';
|
||||||
|
$pass = getenv('DB_PASS') ?: 'changeme';
|
||||||
|
} else {
|
||||||
|
$host = '127.0.0.1';
|
||||||
|
$user = 'root';
|
||||||
|
$pass = '';
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
$familyPdo = new PDO(
|
||||||
$fam_pdo = new PDO("mysql:host=$host;dbname=$db_name;charset=utf8mb4", $user, $pass, [
|
"mysql:host=$host;dbname=$dbName;charset=utf8mb4",
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
$user,
|
||||||
]);
|
$pass,
|
||||||
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||||
|
);
|
||||||
|
// --- FIN DU BLOC MODIFIÉ ---
|
||||||
|
|
||||||
// 3. Injecter la nouvelle table
|
|
||||||
$sql = "
|
$sql = "
|
||||||
CREATE TABLE IF NOT EXISTS pf_foyer_settings (
|
CREATE TABLE IF NOT EXISTS pf_meals_plan (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
currency VARCHAR(10) NOT NULL DEFAULT '€',
|
plan_date DATE NOT NULL,
|
||||||
zone_scolaire VARCHAR(5) NOT NULL DEFAULT 'C',
|
service ENUM('lunch', 'dinner') NOT NULL,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
person_id INT NOT NULL,
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
meal_name VARCHAR(500) DEFAULT NULL,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '€', 'C');
|
UNIQUE KEY uq_meal_plan (plan_date, service, person_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
";
|
";
|
||||||
|
|
||||||
$fam_pdo->exec($sql);
|
$familyPdo->exec($sql);
|
||||||
echo "<span style='color:green'>✅ OK</span><br>";
|
echo "✅ Table pf_meals_plan créée pour $dbName.<br>";
|
||||||
|
}
|
||||||
|
echo "<br>🎉 Migration terminée avec succès.";
|
||||||
|
|
||||||
|
// À ajouter dans migrate.php
|
||||||
|
echo "Migration : Ajout des colonnes dynamiques au Budget...\n";
|
||||||
|
$stmtFam = $meta_pdo->query("SELECT id, db_name FROM families");
|
||||||
|
$families = $stmtFam->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
foreach ($families as $fam) {
|
||||||
|
$dbName = $fam['db_name'];
|
||||||
|
try {
|
||||||
|
$famPdo = new PDO("mysql:host=$meta_host;dbname=$dbName;charset=utf8mb4", $meta_user, $meta_pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||||
|
|
||||||
|
// Vérification si la colonne n'existe pas déjà
|
||||||
|
$checkCol = $famPdo->query("SHOW COLUMNS FROM pf_budget_items LIKE 'is_dynamic'");
|
||||||
|
if ($checkCol->rowCount() == 0) {
|
||||||
|
$famPdo->exec("ALTER TABLE pf_budget_items
|
||||||
|
ADD COLUMN is_dynamic TINYINT(1) DEFAULT 0 AFTER is_estimate,
|
||||||
|
ADD COLUMN dynamic_code VARCHAR(50) DEFAULT NULL AFTER is_dynamic");
|
||||||
|
echo "Famille {$fam['id']} ($dbName) : Colonnes is_dynamic et dynamic_code ajoutées.\n";
|
||||||
|
}
|
||||||
} catch (\PDOException $e) {
|
} catch (\PDOException $e) {
|
||||||
echo "<span style='color:red'>❌ Erreur : " . $e->getMessage() . "</span><br>";
|
echo "Erreur sur la famille {$fam['id']} ($dbName) : " . $e->getMessage() . "\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "<h2>🎉 Migration terminée ! Tu peux supprimer ce fichier.</h2>";
|
} catch (Exception $e) {
|
||||||
?>
|
die("❌ Erreur lors de la migration : " . $e->getMessage());
|
||||||
|
}
|
||||||
+626
@@ -0,0 +1,626 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||||
|
/>
|
||||||
|
<title>HouseHub - Planning Repas</title>
|
||||||
|
<style>
|
||||||
|
/* --- VARIABLES --- */
|
||||||
|
:root {
|
||||||
|
--primary: #3b82f6;
|
||||||
|
--bg-page: #f8fafc;
|
||||||
|
--bg-panel: #ffffff;
|
||||||
|
--bg-subtle: #f1f5f9;
|
||||||
|
--text-main: #0f172a;
|
||||||
|
--text-muted: #64748b;
|
||||||
|
--border-light: #e2e8f0;
|
||||||
|
--border-strong: #cbd5e1;
|
||||||
|
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||||
|
--shadow-lg: 0 -4px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
--alert-success-bg: #dcfce7;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family:
|
||||||
|
"Inter",
|
||||||
|
-apple-system,
|
||||||
|
sans-serif;
|
||||||
|
background: var(--bg-page);
|
||||||
|
color: var(--text-main);
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px;
|
||||||
|
overscroll-behavior-y: none;
|
||||||
|
padding-bottom: 70px; /* Espace pour la toolbar fixe */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- EN-TÊTE --- */
|
||||||
|
.header-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.header-bar h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.pf-btn {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
color: var(--text-main);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
.pf-btn.primary {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- MATRICE ULTRA-COMPACTE --- */
|
||||||
|
.meals-wrapper {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.meals-table {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.meals-table th,
|
||||||
|
.meals-table td {
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
padding: 0;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.meals-table th {
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
font-size: 0.6rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 6px 2px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-sticky {
|
||||||
|
width: 40px;
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
border-right: 2px solid var(--border-strong) !important;
|
||||||
|
padding: 4px 2px !important;
|
||||||
|
}
|
||||||
|
.day-label {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text-main);
|
||||||
|
text-transform: uppercase;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.day-label.invisible {
|
||||||
|
visibility: hidden;
|
||||||
|
height: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.service-icon {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1rem;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
transition: transform 0.1s;
|
||||||
|
}
|
||||||
|
.service-icon:active {
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-lunch td {
|
||||||
|
border-top: 2px solid var(--border-strong) !important;
|
||||||
|
}
|
||||||
|
.row-dinner td {
|
||||||
|
border-top: 1px dashed var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-input-cell {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
}
|
||||||
|
.meal-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 4px;
|
||||||
|
color: var(--text-main);
|
||||||
|
font-family: inherit;
|
||||||
|
resize: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.meal-input:focus {
|
||||||
|
background: #eff6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- BARRE CONTEXTUELLE (FIXE EN BAS) --- */
|
||||||
|
.context-toolbar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: -80px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 60px;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-top: 1px solid var(--border-strong);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
transition: bottom 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
.context-toolbar.active {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.ctx-btn {
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-main);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.ctx-btn.primary {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- MODALE : COPIER VERS (DESIGN GRILLE) --- */
|
||||||
|
.pf-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(15, 23, 42, 0.6);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 2000;
|
||||||
|
}
|
||||||
|
.pf-modal.open {
|
||||||
|
display: flex;
|
||||||
|
animation: fadeInBg 0.2s;
|
||||||
|
}
|
||||||
|
.pf-modal-content {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
width: 95%;
|
||||||
|
max-width: 600px;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.modal-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
font-weight: 800;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.modal-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1;
|
||||||
|
margin-top: -5px;
|
||||||
|
}
|
||||||
|
.modal-source {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grille des Jours */
|
||||||
|
.week-section-title {
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
text-align: center;
|
||||||
|
padding: 6px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 15px 0 10px 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.days-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-card {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
.day-card-title {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
.day-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.day-action-btn {
|
||||||
|
background: var(--bg-page);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 6px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.1s;
|
||||||
|
}
|
||||||
|
.day-action-btn:active {
|
||||||
|
transform: scale(0.9);
|
||||||
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 380px) {
|
||||||
|
.days-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fadeInBg {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header-bar">
|
||||||
|
<h1>
|
||||||
|
<span style="opacity: 0.5">🍽️</span>
|
||||||
|
<span data-i18n="meals_board_title">Planning Repas</span>
|
||||||
|
</h1>
|
||||||
|
<button class="pf-btn" title="Générer les courses">
|
||||||
|
🛒 <span data-i18n="btn_groceries">Courses</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BARRE CONTEXTUELLE FIXE (Évite le conflit avec le clavier mobile) -->
|
||||||
|
<div class="context-toolbar" id="contextToolbar">
|
||||||
|
<button
|
||||||
|
class="ctx-btn"
|
||||||
|
onmousedown="
|
||||||
|
event.preventDefault();
|
||||||
|
applyToAllInRow();
|
||||||
|
"
|
||||||
|
>
|
||||||
|
➡️ <span data-i18n="btn_apply_all">Tous</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="ctx-btn primary"
|
||||||
|
onmousedown="
|
||||||
|
event.preventDefault();
|
||||||
|
openCopySheetFromCell();
|
||||||
|
"
|
||||||
|
>
|
||||||
|
🔄 <span data-i18n="btn_copy_to">Copier vers...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="meals-wrapper">
|
||||||
|
<table class="meals-table" id="mealsTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="col-sticky"></th>
|
||||||
|
<th>PAPA</th>
|
||||||
|
<th>MAMAN</th>
|
||||||
|
<th>E1</th>
|
||||||
|
<th>E2</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<!-- Injecté via JS -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODALE DE COPIE (GRILLE LUNDI-DIMANCHE) -->
|
||||||
|
<div id="copyModal" class="pf-modal">
|
||||||
|
<div class="pf-modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title">
|
||||||
|
🔄 <span data-i18n="modal_copy_title">Copier vers...</span>
|
||||||
|
</h3>
|
||||||
|
<button class="modal-close" onclick="closeCopyModal()">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="copySourceLabel" class="modal-source">
|
||||||
|
Source : Plat individuel (Lundi Midi)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="week-section-title" data-i18n="week_current">
|
||||||
|
Semaine actuelle
|
||||||
|
</div>
|
||||||
|
<div class="days-grid" id="currentWeekGrid"></div>
|
||||||
|
|
||||||
|
<div class="week-section-title" data-i18n="week_next">
|
||||||
|
Semaine prochaine
|
||||||
|
</div>
|
||||||
|
<div class="days-grid" id="nextWeekGrid"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// --- I18N MOCKUP ---
|
||||||
|
window.I18N = {
|
||||||
|
meals_board_title: "Planning Repas",
|
||||||
|
btn_groceries: "Courses",
|
||||||
|
btn_apply_all: "Tous",
|
||||||
|
btn_copy_to: "Copier vers...",
|
||||||
|
modal_copy_title: "Copier vers...",
|
||||||
|
source_individual: "Plat individuel",
|
||||||
|
source_row: "Repas complet",
|
||||||
|
week_current: "Semaine actuelle",
|
||||||
|
week_next: "Semaine prochaine",
|
||||||
|
};
|
||||||
|
document
|
||||||
|
.querySelectorAll("[data-i18n]")
|
||||||
|
.forEach(
|
||||||
|
(el) => (el.innerHTML = window.I18N[el.getAttribute("data-i18n")]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const days = ["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"];
|
||||||
|
const fullDays = [
|
||||||
|
"Lundi",
|
||||||
|
"Mardi",
|
||||||
|
"Mercredi",
|
||||||
|
"Jeudi",
|
||||||
|
"Vendredi",
|
||||||
|
"Samedi",
|
||||||
|
"Dimanche",
|
||||||
|
];
|
||||||
|
const membersCount = 4;
|
||||||
|
const tbody = document.querySelector("#mealsTable tbody");
|
||||||
|
|
||||||
|
let copyState = {
|
||||||
|
type: null,
|
||||||
|
dayIdx: null,
|
||||||
|
service: null,
|
||||||
|
personIdx: null,
|
||||||
|
sourceElements: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 1. GÉNÉRATION DE LA GRILLE PRINCIPALE ---
|
||||||
|
days.forEach((day, index) => {
|
||||||
|
let trLunch = document.createElement("tr");
|
||||||
|
trLunch.className = "row-lunch";
|
||||||
|
trLunch.innerHTML = `
|
||||||
|
<td class="col-sticky">
|
||||||
|
<span class="day-label">${day}</span>
|
||||||
|
<div class="service-icon" onclick="openCopySheetFromRow(${index}, 'lunch')" title="Copier la ligne">☀️</div>
|
||||||
|
</td>`;
|
||||||
|
for (let p = 0; p < membersCount; p++) {
|
||||||
|
trLunch.innerHTML += `<td class="meal-input-cell"><textarea class="meal-input js-meal-input" data-d="${index}" data-s="lunch" data-p="${p}" placeholder="-"></textarea></td>`;
|
||||||
|
}
|
||||||
|
tbody.appendChild(trLunch);
|
||||||
|
|
||||||
|
let trDinner = document.createElement("tr");
|
||||||
|
trDinner.className = "row-dinner";
|
||||||
|
trDinner.innerHTML = `
|
||||||
|
<td class="col-sticky">
|
||||||
|
<span class="day-label invisible">${day}</span>
|
||||||
|
<div class="service-icon" onclick="openCopySheetFromRow(${index}, 'dinner')" title="Copier la ligne">🌙</div>
|
||||||
|
</td>`;
|
||||||
|
for (let p = 0; p < membersCount; p++) {
|
||||||
|
trDinner.innerHTML += `<td class="meal-input-cell"><textarea class="meal-input js-meal-input" data-d="${index}" data-s="dinner" data-p="${p}" placeholder="-"></textarea></td>`;
|
||||||
|
}
|
||||||
|
tbody.appendChild(trDinner);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- 2. GÉNÉRATION DES GRILLES DE LA MODALE COPIE ---
|
||||||
|
function buildModalGrid(containerId, weekTarget) {
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
let html = "";
|
||||||
|
fullDays.forEach((dayName, idx) => {
|
||||||
|
html += `
|
||||||
|
<div class="day-card">
|
||||||
|
<div class="day-card-title">${dayName}</div>
|
||||||
|
<div class="day-card-actions">
|
||||||
|
<button type="button" class="day-action-btn" onclick="executeCopy(${idx}, 'lunch', '${weekTarget}')">☀️</button>
|
||||||
|
<button type="button" class="day-action-btn" onclick="executeCopy(${idx}, 'dinner', '${weekTarget}')">🌙</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
buildModalGrid("currentWeekGrid", "current");
|
||||||
|
buildModalGrid("nextWeekGrid", "next");
|
||||||
|
|
||||||
|
// --- 3. GESTION DU FOCUS & DE LA TOOLBAR ---
|
||||||
|
document.addEventListener("focusin", (e) => {
|
||||||
|
if (e.target.classList.contains("js-meal-input")) {
|
||||||
|
copyState = {
|
||||||
|
type: "cell",
|
||||||
|
dayIdx: e.target.getAttribute("data-d"),
|
||||||
|
service: e.target.getAttribute("data-s"),
|
||||||
|
personIdx: e.target.getAttribute("data-p"),
|
||||||
|
sourceElements: [e.target],
|
||||||
|
};
|
||||||
|
document.getElementById("contextToolbar").classList.add("active");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("focusout", (e) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!document.activeElement.classList.contains("js-meal-input")) {
|
||||||
|
document
|
||||||
|
.getElementById("contextToolbar")
|
||||||
|
.classList.remove("active");
|
||||||
|
}
|
||||||
|
}, 150);
|
||||||
|
});
|
||||||
|
|
||||||
|
function flashSuccess(input) {
|
||||||
|
input.style.transition = "background 0.3s";
|
||||||
|
input.style.background = "var(--alert-success-bg)";
|
||||||
|
setTimeout(() => (input.style.background = "transparent"), 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 4. ACTIONS DE COPIE ---
|
||||||
|
function applyToAllInRow() {
|
||||||
|
if (
|
||||||
|
copyState.type !== "cell" ||
|
||||||
|
!copyState.sourceElements[0].value.trim()
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
const val = copyState.sourceElements[0].value;
|
||||||
|
const inputs = document.querySelectorAll(
|
||||||
|
`.js-meal-input[data-d="${copyState.dayIdx}"][data-s="${copyState.service}"]`,
|
||||||
|
);
|
||||||
|
inputs.forEach((inp) => {
|
||||||
|
if (inp !== copyState.sourceElements[0]) {
|
||||||
|
inp.value = val;
|
||||||
|
flashSuccess(inp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCopySheetFromCell() {
|
||||||
|
if (
|
||||||
|
!copyState.sourceElements[0] ||
|
||||||
|
!copyState.sourceElements[0].value.trim()
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
document.getElementById("contextToolbar").classList.remove("active");
|
||||||
|
document.activeElement.blur(); // Masque le clavier mobile
|
||||||
|
|
||||||
|
const svcLabel = copyState.service === "lunch" ? "Midi" : "Soir";
|
||||||
|
document.getElementById("copySourceLabel").innerText =
|
||||||
|
`Source : ${window.I18N.source_individual} (${fullDays[copyState.dayIdx]} ${svcLabel})`;
|
||||||
|
document.getElementById("copyModal").classList.add("open");
|
||||||
|
document.body.classList.add("no-scroll");
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCopySheetFromRow(dIdx, service) {
|
||||||
|
const inputs = document.querySelectorAll(
|
||||||
|
`.js-meal-input[data-d="${dIdx}"][data-s="${service}"]`,
|
||||||
|
);
|
||||||
|
const hasContent = Array.from(inputs).some(
|
||||||
|
(inp) => inp.value.trim() !== "",
|
||||||
|
);
|
||||||
|
if (!hasContent) return;
|
||||||
|
|
||||||
|
copyState = {
|
||||||
|
type: "row",
|
||||||
|
dayIdx: dIdx,
|
||||||
|
service: service,
|
||||||
|
sourceElements: Array.from(inputs),
|
||||||
|
};
|
||||||
|
const svcLabel = service === "lunch" ? "Midi" : "Soir";
|
||||||
|
document.getElementById("copySourceLabel").innerText =
|
||||||
|
`Source : ${window.I18N.source_row} (${fullDays[dIdx]} ${svcLabel})`;
|
||||||
|
document.getElementById("copyModal").classList.add("open");
|
||||||
|
document.body.classList.add("no-scroll");
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCopyModal() {
|
||||||
|
document.getElementById("copyModal").classList.remove("open");
|
||||||
|
document.body.classList.remove("no-scroll");
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeCopy(targetDayIdx, targetService, targetWeek) {
|
||||||
|
if (targetWeek === "next") {
|
||||||
|
alert("Simulation : Copié avec succès vers la SEMAINE PROCHAINE !");
|
||||||
|
closeCopyModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copyState.type === "cell") {
|
||||||
|
const val = copyState.sourceElements[0].value;
|
||||||
|
const targetInput = document.querySelector(
|
||||||
|
`.js-meal-input[data-d="${targetDayIdx}"][data-s="${targetService}"][data-p="${copyState.personIdx}"]`,
|
||||||
|
);
|
||||||
|
if (targetInput) {
|
||||||
|
targetInput.value = val;
|
||||||
|
flashSuccess(targetInput);
|
||||||
|
}
|
||||||
|
} else if (copyState.type === "row") {
|
||||||
|
const targetInputs = document.querySelectorAll(
|
||||||
|
`.js-meal-input[data-d="${targetDayIdx}"][data-s="${targetService}"]`,
|
||||||
|
);
|
||||||
|
copyState.sourceElements.forEach((inp, idx) => {
|
||||||
|
if (inp.value.trim() !== "") {
|
||||||
|
targetInputs[idx].value = inp.value;
|
||||||
|
flashSuccess(targetInputs[idx]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
closeCopyModal();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+1804
-167
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,260 @@
|
|||||||
|
<?php
|
||||||
|
// modules/budget/includes/DynamicBudgetCalculator.php
|
||||||
|
|
||||||
|
class DynamicBudgetCalculator {
|
||||||
|
private \PDO $pdo;
|
||||||
|
private int $year;
|
||||||
|
private string $month;
|
||||||
|
private array $settings = [];
|
||||||
|
private array $foyerSettings = [];
|
||||||
|
|
||||||
|
private array $kids = [];
|
||||||
|
private string $helperName = 'Intervenant'; // Valeur par défaut
|
||||||
|
private array $calendarEvents = [];
|
||||||
|
|
||||||
|
public function __construct(\PDO $pdo, int|string $year, int|string $month) {
|
||||||
|
$this->pdo = $pdo;
|
||||||
|
$this->year = (int)$year;
|
||||||
|
$this->month = str_pad((string)$month, 2, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
$this->loadSettings();
|
||||||
|
$this->loadPeople();
|
||||||
|
$this->loadEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadSettings(): void {
|
||||||
|
$stmt = $this->pdo->query("SELECT setting_key, setting_value FROM pf_settings WHERE module = 'budget'");
|
||||||
|
$this->settings = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
|
$this->foyerSettings = $this->pdo->query("SELECT * FROM pf_foyer_settings LIMIT 1")->fetch(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSetting(string $key, float|int $default = 0): float {
|
||||||
|
return isset($this->settings[$key]) ? (float)$this->settings[$key] : (float)$default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadPeople(): void {
|
||||||
|
// 1. Chargement des enfants sans doublons
|
||||||
|
$stmt = $this->pdo->query("SELECT id, name, care_modes FROM pf_people WHERE role IN ('enfant', 'child') AND is_active = 1");
|
||||||
|
$rawKids = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$this->kids = [];
|
||||||
|
$seenNames = [];
|
||||||
|
|
||||||
|
foreach ($rawKids as $k) {
|
||||||
|
$nameKey = mb_strtolower(trim($k['name']));
|
||||||
|
if (!isset($seenNames[$nameKey])) {
|
||||||
|
$this->kids[] = $k;
|
||||||
|
$seenNames[$nameKey] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Chargement dynamique de l'intervenant (ex: Carole)
|
||||||
|
$stmtHelper = $this->pdo->query("SELECT name FROM pf_people WHERE role IN ('helper', 'nounou') AND is_active = 1 LIMIT 1");
|
||||||
|
$helper = $stmtHelper->fetchColumn();
|
||||||
|
if ($helper) {
|
||||||
|
$this->helperName = mb_strtoupper($helper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadEvents(): void {
|
||||||
|
$startDate = "{$this->year}-{$this->month}-01";
|
||||||
|
$endDate = date('Y-m-t', strtotime($startDate));
|
||||||
|
$stmt = $this->pdo->prepare("SELECT event_date, event_type, person_id FROM pf_events WHERE event_date BETWEEN ? AND ?");
|
||||||
|
$stmt->execute([$startDate, $endDate]);
|
||||||
|
$events = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
foreach ($events as $e) {
|
||||||
|
$this->calendarEvents[$e['event_date']][] = $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getOffDays(): array {
|
||||||
|
$cacheKey = "offdays_{$this->year}";
|
||||||
|
$stmt = $this->pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'system_cache' AND reference_id = ?");
|
||||||
|
$stmt->execute([$cacheKey]);
|
||||||
|
$cached = $stmt->fetchColumn();
|
||||||
|
|
||||||
|
if ($cached) return json_decode($cached, true);
|
||||||
|
|
||||||
|
$offDays = ['feries' => [], 'vacances' => []];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$jsonFeries = @file_get_contents("https://calendrier.api.gouv.fr/jours-feries/metropole/{$this->year}.json");
|
||||||
|
if ($jsonFeries) $offDays['feries'] = array_keys(json_decode($jsonFeries, true));
|
||||||
|
} catch (\Throwable $e) {}
|
||||||
|
|
||||||
|
$zone = $this->foyerSettings['zone_scolaire'] ?? 'C';
|
||||||
|
try {
|
||||||
|
$url = "https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records?where=annee_scolaire='{$this->year}-" . ($this->year + 1) . "' AND zones LIKE '%Zone {$zone}%'&limit=100";
|
||||||
|
$jsonVacances = @file_get_contents($url);
|
||||||
|
if ($jsonVacances) {
|
||||||
|
$data = json_decode($jsonVacances, true);
|
||||||
|
if (!empty($data['results'])) {
|
||||||
|
foreach ($data['results'] as $r) {
|
||||||
|
$start = new DateTime(substr($r['start_date'], 0, 10));
|
||||||
|
$end = new DateTime(substr($r['end_date'], 0, 10));
|
||||||
|
if ($start->format('N') == 5) $start->modify('+1 day');
|
||||||
|
while ($start < $end) {
|
||||||
|
$offDays['vacances'][] = $start->format('Y-m-d');
|
||||||
|
$start->modify('+1 day');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {}
|
||||||
|
|
||||||
|
$this->pdo->prepare("INSERT INTO pf_notes (note_type, reference_id, content) VALUES ('system_cache', ?, ?) ON DUPLICATE KEY UPDATE content = VALUES(content)")
|
||||||
|
->execute([$cacheKey, json_encode($offDays)]);
|
||||||
|
|
||||||
|
return $offDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEstimate(string $dynamicCode): array {
|
||||||
|
if ($dynamicCode === 'SCHOOL_ESTIMATE') {
|
||||||
|
return $this->calculateSchoolEstimate();
|
||||||
|
}
|
||||||
|
return ['amount' => 0.0, 'details' => tr('error_occured')];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function calculateSchoolEstimate(): array {
|
||||||
|
$nannyFixed = $this->getSetting('budget_nanny_fixed', 700);
|
||||||
|
$nannyDaily = $this->getSetting('budget_nanny_daily', 4);
|
||||||
|
$nannyAid = $this->getSetting('budget_nanny_aid', 120);
|
||||||
|
$cesuAvg = $this->getSetting('budget_nanny_cesu_avg', 170);
|
||||||
|
|
||||||
|
$schoolMeal = $this->getSetting('budget_school_meal', 5.62);
|
||||||
|
$schoolAftercare = $this->getSetting('budget_school_aftercare', 1.97);
|
||||||
|
$schoolFullday = $this->getSetting('budget_school_fullday', 21.89);
|
||||||
|
|
||||||
|
$offDays = $this->getOffDays();
|
||||||
|
|
||||||
|
$totalCost = 0;
|
||||||
|
$details = sprintf(tr('bud_audit_month') . "\n\n", $this->month, $this->year);
|
||||||
|
|
||||||
|
$hasNanny = false;
|
||||||
|
$nannyOffDays = 0;
|
||||||
|
$nannyGlobalCost = $nannyFixed - $nannyAid - $cesuAvg;
|
||||||
|
|
||||||
|
$kidsStats = [];
|
||||||
|
|
||||||
|
// 1. Initialisation stricte
|
||||||
|
foreach ($this->kids as $k) {
|
||||||
|
$modes = json_decode($k['care_modes'] ?? '[]', true) ?: [];
|
||||||
|
$isNanny = count(array_intersect(array_map('strtolower', $modes), ['nounou', 'kangur'])) > 0;
|
||||||
|
if ($isNanny) $hasNanny = true;
|
||||||
|
|
||||||
|
$kidsStats[$k['id']] = [
|
||||||
|
'name' => $k['name'], 'isNanny' => $isNanny,
|
||||||
|
'nannyDays' => 0, 'sick' => 0,
|
||||||
|
'schoolMeal' => 0, 'schoolAftercare' => 0, 'center' => 0
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$daysInMonth = cal_days_in_month(CAL_GREGORIAN, (int)$this->month, $this->year);
|
||||||
|
|
||||||
|
// 2. Itération SANS pointeurs de référence
|
||||||
|
for ($d = 1; $d <= $daysInMonth; $d++) {
|
||||||
|
$dateStr = sprintf("%04d-%02d-%02d", $this->year, $this->month, $d);
|
||||||
|
$dayOfWeek = date('N', strtotime($dateStr));
|
||||||
|
|
||||||
|
if ($dayOfWeek >= 6) continue;
|
||||||
|
if (in_array($dateStr, $offDays['feries'])) continue;
|
||||||
|
|
||||||
|
$isVacances = in_array($dateStr, $offDays['vacances']);
|
||||||
|
$events = $this->calendarEvents[$dateStr] ?? [];
|
||||||
|
|
||||||
|
$nannyOffToday = false;
|
||||||
|
foreach ($events as $e) {
|
||||||
|
if (in_array($e['event_type'], ['HELPER_OFF', 'HELPER_EXTRA'])) $nannyOffToday = true;
|
||||||
|
}
|
||||||
|
if ($nannyOffToday) $nannyOffDays++;
|
||||||
|
|
||||||
|
foreach ($this->kids as $kid) {
|
||||||
|
$kidId = $kid['id'];
|
||||||
|
|
||||||
|
$isSick = false;
|
||||||
|
$hasCenterEvent = false;
|
||||||
|
foreach ($events as $e) {
|
||||||
|
if ($e['event_type'] === 'CHILD_SICK' && $e['person_id'] == $kidId) $isSick = true;
|
||||||
|
if (strtoupper($e['event_type']) === 'CENTRE' && $e['person_id'] == $kidId) $hasCenterEvent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isSick) {
|
||||||
|
$kidsStats[$kidId]['sick']++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($kidsStats[$kidId]['isNanny']) {
|
||||||
|
if (!$nannyOffToday) $kidsStats[$kidId]['nannyDays']++;
|
||||||
|
} else {
|
||||||
|
if ($isVacances) {
|
||||||
|
if ($hasCenterEvent) $kidsStats[$kidId]['center']++;
|
||||||
|
} else {
|
||||||
|
if ($dayOfWeek == 3) {
|
||||||
|
$kidsStats[$kidId]['center']++;
|
||||||
|
} else {
|
||||||
|
$kidsStats[$kidId]['schoolMeal']++;
|
||||||
|
if (in_array($dayOfWeek, [2, 4])) $kidsStats[$kidId]['schoolAftercare']++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalNannyDays = 0;
|
||||||
|
foreach ($kidsStats as $stat) {
|
||||||
|
if ($stat['isNanny']) {
|
||||||
|
$totalNannyDays += $stat['nannyDays'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalNannyFees = $totalNannyDays * $nannyDaily;
|
||||||
|
$nannyGlobalCost = $nannyFixed + $totalNannyFees - $nannyAid - $cesuAvg;
|
||||||
|
|
||||||
|
// 3. Rédaction du Bilan
|
||||||
|
if ($hasNanny) {
|
||||||
|
$totalCost += max(0, $nannyGlobalCost);
|
||||||
|
$details .= sprintf(tr('bud_audit_nanny_full') . "\n",
|
||||||
|
$this->helperName, $nannyFixed, $totalNannyDays, $nannyDaily, $totalNannyFees, $nannyAid, $cesuAvg, max(0, $nannyGlobalCost));
|
||||||
|
|
||||||
|
if ($nannyOffDays > 0) {
|
||||||
|
$details .= sprintf(tr('bud_audit_nanny_abs') . "\n", $this->helperName, $nannyOffDays);
|
||||||
|
}
|
||||||
|
$details .= "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($kidsStats as $finalStat) {
|
||||||
|
if ($finalStat['isNanny']) {
|
||||||
|
// L'enfant est chez la nounou : on ne l'affiche QUE s'il a été malade
|
||||||
|
if ($finalStat['sick'] > 0) {
|
||||||
|
$details .= sprintf(tr('bud_audit_child') . "\n", mb_strtoupper($finalStat['name']));
|
||||||
|
$details .= sprintf(tr('bud_audit_sick') . "\n", $finalStat['sick']) . "\n\n";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// L'enfant va à l'école / centre
|
||||||
|
$details .= sprintf(tr('bud_audit_child') . "\n", mb_strtoupper($finalStat['name']));
|
||||||
|
|
||||||
|
$costM = $finalStat['schoolMeal'] * $schoolMeal;
|
||||||
|
$costA = $finalStat['schoolAftercare'] * $schoolAftercare;
|
||||||
|
$costC = $finalStat['center'] * $schoolFullday;
|
||||||
|
$kidTotal = $costM + $costA + $costC;
|
||||||
|
|
||||||
|
$totalCost += $kidTotal;
|
||||||
|
|
||||||
|
$details .= sprintf(tr('bud_audit_school_fee') . "\n",
|
||||||
|
$finalStat['schoolMeal'], $finalStat['schoolAftercare'], $finalStat['center'], $kidTotal);
|
||||||
|
|
||||||
|
if ($finalStat['sick'] > 0) {
|
||||||
|
$details .= sprintf(tr('bud_audit_sick') . "\n", $finalStat['sick']) . "\n";
|
||||||
|
}
|
||||||
|
$details .= "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'amount' => round($totalCost, 2),
|
||||||
|
'details' => trim($details)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
// On charge l'authentification et l'init PDO de l'espace familial courant automatiquement via tes includes globaux
|
||||||
|
require_once __DIR__ . '/../../../../includes/auth.php';
|
||||||
|
require_once __DIR__ . '/../../../../includes/db.php';
|
||||||
|
require_once __DIR__ . '/../../../../includes/i18n.php';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$title = trim($_POST['title'] ?? '');
|
||||||
|
$amount = floatval($_POST['amount'] ?? 0);
|
||||||
|
$expected_date = $_POST['expected_date'] ?? '';
|
||||||
|
|
||||||
|
if (empty($title) || $amount <= 0 || empty($expected_date)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => tr('error_generic')]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Grâce au multi-tenant par BDD, $pdo pointe déjà sur la base de la famille connectée
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pf_expected_expenses (title, amount, expected_date) VALUES (?, ?, ?)");
|
||||||
|
$stmt->execute([$title, $amount, $expected_date]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => tr('success_add_provision')]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../../../../includes/auth.php';
|
||||||
|
require_once __DIR__ . '/../../../../includes/db.php';
|
||||||
|
|
||||||
|
$id = intval($_POST['id'] ?? 0);
|
||||||
|
|
||||||
|
if (!$id) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Invalid ID']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM pf_expected_expenses WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
Whitespace-only changes.
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../../../../includes/auth.php';
|
||||||
|
require_once __DIR__ . '/../../../../includes/db.php';
|
||||||
|
require_once __DIR__ . '/../../../../includes/i18n.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Récupérer les provisions non payées, classées par date
|
||||||
|
$stmt = $pdo->query("SELECT id, title, amount, expected_date FROM pf_expected_expenses WHERE is_paid = 0 ORDER BY expected_date ASC");
|
||||||
|
$provisions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (empty($provisions)) {
|
||||||
|
$html = '<p class="pf-text-muted">' . tr('no_provisions') . '</p>';
|
||||||
|
} else {
|
||||||
|
$html = '<table class="pf-table" style="width:100%; border-collapse: collapse;">';
|
||||||
|
$html .= '<thead style="border-bottom: 2px solid var(--border-color); text-align: left;">';
|
||||||
|
$html .= '<tr><th>' . tr('provision_label') . '</th><th>' . tr('amount') . '</th><th>' . tr('expected_date') . '</th><th style="text-align:right;">Actions</th></tr>';
|
||||||
|
$html .= '</thead><tbody>';
|
||||||
|
|
||||||
|
foreach ($provisions as $p) {
|
||||||
|
$dateFormated = date('d/m/Y', strtotime($p['expected_date']));
|
||||||
|
$html .= '<tr style="border-bottom: 1px solid var(--border-color); height: 45px;">';
|
||||||
|
$html .= '<td>' . htmlspecialchars($p['title']) . '</td>';
|
||||||
|
$html .= '<td>' . number_format($p['amount'], 2, ',', ' ') . ' €</td>';
|
||||||
|
$html .= '<td>' . $dateFormated . '</td>';
|
||||||
|
$html .= '<td style="text-align:right;"><button class="pf-btn pf-btn-danger btn-delete-provision" data-id="' . $p['id'] . '">' . tr('btn_delete') . '</button></td>';
|
||||||
|
$html .= '</tr>';
|
||||||
|
}
|
||||||
|
$html .= '</tbody></table>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'html' => $html]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -6,43 +6,54 @@ require_login();
|
|||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$action = $_POST['action'] ?? '';
|
$action = $_POST['action'] ?? '';
|
||||||
|
|
||||||
// --- ACTION : SAUVEGARDER (AJOUT OU MODIF) ---
|
// --- ACTION : SAUVEGARDER (AJOUT OU MODIF DU BUDGET) ---
|
||||||
if ($action === 'save') {
|
if ($action === 'save') {
|
||||||
$id = $_POST['id'] ?? '';
|
try {
|
||||||
$name = $_POST['name'];
|
// 1. Sécurisation des données entrantes
|
||||||
$category = $_POST['category'];
|
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
|
||||||
$type = $_POST['type'];
|
$name = trim($_POST['name'] ?? '');
|
||||||
$payment_day = empty($_POST['payment_day']) ? null : (int)$_POST['payment_day'];
|
$category = $_POST['category'] ?? '';
|
||||||
$is_estimate = $_POST['is_estimate'];
|
$type = $_POST['type'] ?? 'Mensuel';
|
||||||
$reg_month = $_POST['reg_month'];
|
$payment_day = !empty($_POST['payment_day']) ? (int)$_POST['payment_day'] : null;
|
||||||
$keywords = $_POST['mapping_keywords'] ?? '';
|
$is_estimate = isset($_POST['is_estimate']) ? (int)$_POST['is_estimate'] : 0;
|
||||||
$holiday_id = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
|
$reg_month = !empty($_POST['reg_month']) ? trim($_POST['reg_month']) : null;
|
||||||
|
$keywords = trim($_POST['mapping_keywords'] ?? '');
|
||||||
|
$holiday_id = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
|
||||||
|
|
||||||
// ==========================================
|
// 2. Gestion du montant (toujours stocker les dépenses en négatif)
|
||||||
// NOUVELLE NORME COMPTABLE : Dépense = Négatif
|
$amount = abs((float)($_POST['amount'] ?? 0));
|
||||||
// ==========================================
|
$amount = -$amount; // On force le négatif car le modal Recap ne gère que des charges
|
||||||
// On récupère le montant en valeur absolue (pour éviter les erreurs si l'utilisateur a tapé un '-')
|
|
||||||
$amount = abs((float)$_POST['amount']);
|
|
||||||
|
|
||||||
// Si c'est un frais, on l'enregistre en négatif
|
// 3. Exécution de la requête
|
||||||
if ($category === 'expense') {
|
if ($id) {
|
||||||
$amount = -$amount;
|
// UPDATE
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_budget_items SET name=?, amount=?, category=?, type=?, payment_day=?, is_estimate=?, reg_month=?, mapping_keywords=?, holiday_id=? WHERE id=?");
|
||||||
|
$stmt->execute([$name, $amount, $category, $type, $payment_day, $is_estimate, $reg_month, $keywords, $holiday_id, $id]);
|
||||||
|
} else {
|
||||||
|
// INSERT
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pf_budget_items (name, amount, category, type, payment_day, is_estimate, reg_month, mapping_keywords, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
|
$stmt->execute([$name, $amount, $category, $type, $payment_day, $is_estimate, $reg_month, $keywords, $holiday_id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Réponse appropriée (JSON si appel via JS fetch, Redirection sinon)
|
||||||
|
if (!empty($_POST['ajax'])) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
header('Location: /budget.php?tab=recap');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// En cas d'erreur BDD (ex: contrainte de clé étrangère, colonne manquante)
|
||||||
|
if (!empty($_POST['ajax'])) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Erreur BDD : ' . $e->getMessage()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
die("Erreur fatale : " . $e->getMessage());
|
||||||
}
|
}
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
if ($id) {
|
|
||||||
// UPDATE
|
|
||||||
$stmt = $pdo->prepare("UPDATE pf_budget_items SET name=?, amount=?, category=?, type=?, payment_day=?, is_estimate=?, reg_month=?, mapping_keywords=?, holiday_id=? WHERE id=?");
|
|
||||||
$stmt->execute([$name, $amount, $category, $type, $payment_day, $is_estimate, $reg_month, $keywords, $holiday_id, $id]);
|
|
||||||
} else {
|
|
||||||
// INSERT
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO pf_budget_items (name, amount, category, type, payment_day, is_estimate, reg_month, mapping_keywords, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
||||||
$stmt->execute([$name, $amount, $category, $type, $payment_day, $is_estimate, $reg_month, $keywords, $holiday_id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirection
|
|
||||||
header('Location: /budget.php?tab=recap');
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ACTION : COCHER/DÉCOCHER RAPIDE (VIA JS FETCH) ---
|
// --- ACTION : COCHER/DÉCOCHER RAPIDE (VIA JS FETCH) ---
|
||||||
@@ -59,7 +70,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ACTION : SUPPRIMER ---
|
// --- ACTION : SUPPRIMER UNE LIGNE DU BUDGET ---
|
||||||
if ($action === 'delete') {
|
if ($action === 'delete') {
|
||||||
$id = $_POST['id'];
|
$id = $_POST['id'];
|
||||||
$pdo->prepare("DELETE FROM pf_budget_items WHERE id = ?")->execute([$id]);
|
$pdo->prepare("DELETE FROM pf_budget_items WHERE id = ?")->execute([$id]);
|
||||||
@@ -84,7 +95,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
|
|
||||||
// --- ACTION : SAUVEGARDER UNE DÉPENSE MANUELLE (FETCH) ---
|
// --- ACTION : SAUVEGARDER UNE DÉPENSE MANUELLE (FETCH) ---
|
||||||
if ($action === 'save_expense_manual') {
|
if ($action === 'save_expense_manual') {
|
||||||
header('Content-Type: application/json'); // On force le retour JSON pour notre script JS
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$id = !empty($_POST['expense_id']) ? (int)$_POST['expense_id'] : null;
|
$id = !empty($_POST['expense_id']) ? (int)$_POST['expense_id'] : null;
|
||||||
@@ -92,26 +103,31 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$amount = floatval($_POST['amount'] ?? 0);
|
$amount = floatval($_POST['amount'] ?? 0);
|
||||||
$date = $_POST['date'] ?? date('Y-m-d');
|
$date = $_POST['date'] ?? date('Y-m-d');
|
||||||
|
|
||||||
// 🛡️ SÉCURITÉ : Format attendu pour gestion_month (qui vient de input type="month") : YYYY-MM.
|
// Format attendu : YYYY-MM
|
||||||
$gestionMonthRaw = $_POST['gestion_month'] ?? '';
|
$gestionMonthRaw = $_POST['gestion_month'] ?? '';
|
||||||
|
|
||||||
// Si la valeur est vide ou '0000-00-00'
|
// Si la valeur est vide ou invalide
|
||||||
if (empty($gestionMonthRaw) || strpos($gestionMonthRaw, '0000') !== false) {
|
if (empty($gestionMonthRaw) || strpos($gestionMonthRaw, '0000') !== false) {
|
||||||
// On force le mois de gestion au 1er jour de la date de la dépense
|
|
||||||
$gestionMonth = date('Y-m-01', strtotime($date));
|
$gestionMonth = date('Y-m-01', strtotime($date));
|
||||||
} else {
|
} else {
|
||||||
// Si la valeur vient d'un input "month" (ex: "2026-04"), on rajoute "-01"
|
|
||||||
$gestionMonth = (strlen($gestionMonthRaw) === 7) ? $gestionMonthRaw . '-01' : $gestionMonthRaw;
|
$gestionMonth = (strlen($gestionMonthRaw) === 7) ? $gestionMonthRaw . '-01' : $gestionMonthRaw;
|
||||||
}
|
}
|
||||||
|
|
||||||
$label = trim($_POST['label'] ?? '');
|
$label = trim($_POST['label'] ?? '');
|
||||||
|
$rawItemId = $_POST['budget_item_id'] ?? '';
|
||||||
$budgetItemId = null;
|
$budgetItemId = null;
|
||||||
|
$salaryId = null;
|
||||||
$holidayId = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
|
$holidayId = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
|
||||||
|
|
||||||
if ($cat === 'School' && !empty($_POST['label_select'])) {
|
if ($cat === 'School' && !empty($_POST['label_select'])) {
|
||||||
$label = trim($_POST['label_select']);
|
$label = trim($_POST['label_select']);
|
||||||
} elseif (($cat === 'Frais' || $cat === 'Income') && !empty($_POST['budget_item_id'])) {
|
} elseif (!empty($rawItemId)) {
|
||||||
$budgetItemId = (int)$_POST['budget_item_id'];
|
// 🔥 On intercepte si c'est un Salaire (SAL_x) ou une Charge Fixe classique (x)
|
||||||
|
if (strpos($rawItemId, 'SAL_') === 0) {
|
||||||
|
$salaryId = (int)str_replace('SAL_', '', $rawItemId);
|
||||||
|
} else {
|
||||||
|
$budgetItemId = (int)$rawItemId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérification de sécurité
|
// Vérification de sécurité
|
||||||
@@ -124,14 +140,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$finalAmount = $is_credit ? abs($amount) : -abs($amount);
|
$finalAmount = $is_credit ? abs($amount) : -abs($amount);
|
||||||
|
|
||||||
if ($id) {
|
if ($id) {
|
||||||
// UPDATE
|
// UPDATE (Ajout de salary_id)
|
||||||
$pdo->prepare("UPDATE pf_expenses SET date_exp=?, gestion_month=?, category=?, label=?, amount=?, budget_item_id=?, holiday_id=? WHERE id=?")
|
$pdo->prepare("UPDATE pf_expenses SET date_exp=?, gestion_month=?, category=?, label=?, amount=?, budget_item_id=?, holiday_id=?, salary_id=? WHERE id=?")
|
||||||
->execute([$date, $gestionMonth, $cat, $label, $finalAmount, $budgetItemId, $holidayId, $id]);
|
->execute([$date, $gestionMonth, $cat, $label, $finalAmount, $budgetItemId, $holidayId, $salaryId, $id]);
|
||||||
} else {
|
} else {
|
||||||
// INSERT
|
// INSERT (Ajout de salary_id)
|
||||||
$uniqueRef = "MANUAL_" . uniqid();
|
$uniqueRef = "MANUAL_" . uniqid();
|
||||||
$pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
$pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id, salary_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||||
->execute([$date, $gestionMonth, $cat, $label, $finalAmount, $uniqueRef, $budgetItemId, $holidayId]);
|
->execute([$date, $gestionMonth, $cat, $label, $finalAmount, $uniqueRef, $budgetItemId, $holidayId, $salaryId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../../../../includes/auth.php';
|
||||||
|
require_once __DIR__ . '/../../../../includes/db.php';
|
||||||
|
require_once __DIR__ . '/../../../../includes/i18n.php';
|
||||||
|
|
||||||
|
$savings_inputs = $_POST['savings'] ?? [];
|
||||||
|
$current_year = date('Y');
|
||||||
|
$current_month = date('m');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Récupération des dépenses prévues
|
||||||
|
$stmtExpenses = $pdo->prepare("
|
||||||
|
SELECT title, amount, expected_date
|
||||||
|
FROM pf_expected_expenses
|
||||||
|
WHERE is_paid = 0
|
||||||
|
AND YEAR(expected_date) = ?
|
||||||
|
AND MONTH(expected_date) = ?
|
||||||
|
ORDER BY expected_date ASC
|
||||||
|
");
|
||||||
|
$stmtExpenses->execute([$current_year, $current_month]);
|
||||||
|
$expenses = $stmtExpenses->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$q1_expenses_total = 0;
|
||||||
|
$q2_expenses_total = 0;
|
||||||
|
|
||||||
|
$detail_html = '<ul style="padding-left: 1.2rem; margin-bottom: 1rem; color: var(--text-muted); font-size:0.9rem;">';
|
||||||
|
foreach ($expenses as $e) {
|
||||||
|
$day = intval(date('d', strtotime($e['expected_date'])));
|
||||||
|
if ($day <= 15) {
|
||||||
|
$q1_expenses_total += $e['amount'];
|
||||||
|
} else {
|
||||||
|
$q2_expenses_total += $e['amount'];
|
||||||
|
}
|
||||||
|
$detail_html .= '<li>' . htmlspecialchars($e['title']) . ' (' . number_format($e['amount'], 2, ',', ' ') . ' € le ' . date('d/m', strtotime($e['expected_date'])) . ')</li>';
|
||||||
|
}
|
||||||
|
$detail_html .= '</ul>';
|
||||||
|
|
||||||
|
$total_expenses = $q1_expenses_total + $q2_expenses_total;
|
||||||
|
|
||||||
|
// 2. Récupération des dettes
|
||||||
|
$stmtDebts = $pdo->query("SELECT payer, SUM(amount) as total_debt FROM pf_advances WHERE is_resolved = 0 GROUP BY payer");
|
||||||
|
$debts = $stmtDebts->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
|
|
||||||
|
// 3. NOUVELLE LOGIQUE : Clearing intelligent (Compensation partielle ou totale)
|
||||||
|
$total_base_inputs = array_sum(array_map('floatval', $savings_inputs));
|
||||||
|
$surplus_for_clearing = $total_base_inputs - $total_expenses;
|
||||||
|
$monthly_savings = 0; // Le vrai total qui sera viré après déduction des dettes
|
||||||
|
|
||||||
|
$contributions_html = '<ul style="padding-left: 1.2rem; margin-bottom: 0.5rem; color: var(--text-main); font-size:0.9rem; line-height: 1.6;">';
|
||||||
|
|
||||||
|
foreach ($savings_inputs as $person => $amount) {
|
||||||
|
$base_amount = floatval($amount);
|
||||||
|
$debt = $debts[$person] ?? 0;
|
||||||
|
|
||||||
|
// Formatage bancaire propre pour les textes
|
||||||
|
$base_fmt = number_format($base_amount, 2, ',', ' ');
|
||||||
|
|
||||||
|
if ($debt > 0) {
|
||||||
|
if ($surplus_for_clearing > 0) {
|
||||||
|
$compensation = min($debt, $base_amount, $surplus_for_clearing);
|
||||||
|
$net_amount = $base_amount - $compensation;
|
||||||
|
$surplus_for_clearing -= $compensation;
|
||||||
|
$monthly_savings += $net_amount;
|
||||||
|
|
||||||
|
$comp_fmt = number_format($compensation, 2, ',', ' ');
|
||||||
|
$net_fmt = number_format($net_amount, 2, ',', ' ');
|
||||||
|
|
||||||
|
$reste_dette = $debt - $compensation;
|
||||||
|
$txt_reste = ($reste_dette > 0)
|
||||||
|
? ' <span style="color:var(--danger); font-size:0.8rem; margin-left: 5px;">(Reste ' . number_format($reste_dette, 2, ',', ' ') . ' € de dette)</span>'
|
||||||
|
: ' <span style="color:var(--success); font-weight:bold; font-size:0.85rem; margin-left: 5px;">(Dette soldée 🎉)</span>';
|
||||||
|
|
||||||
|
// Harmonisation en bleu (#2563eb) pour le montant net à verser
|
||||||
|
$contributions_html .= '<li style="margin-bottom: 5px;"><strong>' . htmlspecialchars($person) . '</strong> : Base ' . $base_fmt . ' € − Remboursé ' . $comp_fmt . ' € = <strong style="color:#2563eb;">' . $net_fmt . ' € à verser</strong>' . $txt_reste . '</li>';
|
||||||
|
} else {
|
||||||
|
$monthly_savings += $base_amount;
|
||||||
|
$contributions_html .= '<li style="margin-bottom: 5px;"><strong>' . htmlspecialchars($person) . '</strong> : <strong style="color:#2563eb;">' . $base_fmt . ' € à verser</strong> <span style="color:var(--danger); font-size:0.8rem; margin-left: 5px;">(Dette gelée, liquidités insuffisantes)</span></li>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$monthly_savings += $base_amount;
|
||||||
|
$contributions_html .= '<li style="margin-bottom: 5px;"><strong>' . htmlspecialchars($person) . '</strong> : <strong style="color:#2563eb;">' . $base_fmt . ' € à verser</strong></li>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$contributions_html .= '</ul>';
|
||||||
|
$contributions_html .= '<p style="color: var(--text-main); font-weight: bold; margin-top:5px; font-size:0.95rem;">Total net atterrissant sur les comptes : ' . number_format($monthly_savings, 2, ',', ' ') . ' €</p>';
|
||||||
|
|
||||||
|
if(empty($expenses)) {
|
||||||
|
$detail_html = '<p style="color: var(--success); font-weight:bold; margin-bottom:1rem; font-size:0.9rem;">🎉 Aucune grosse dépense enregistrée sur ce mois.</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- MOTEUR DE RÈGLES DES QUINZAINES (Remplacement des ** par <strong>) ---
|
||||||
|
$instructions = [];
|
||||||
|
$remaining_to_allocate = $monthly_savings;
|
||||||
|
|
||||||
|
// Règle 1 : Première quinzaine
|
||||||
|
if ($q1_expenses_total > 0) {
|
||||||
|
if ($remaining_to_allocate >= $q1_expenses_total) {
|
||||||
|
$remaining_to_allocate -= $q1_expenses_total;
|
||||||
|
$instructions[] = "💼 <strong>Dès le 1er</strong> : Laissez <strong>" . number_format($q1_expenses_total, 2, ',', ' ') . " €</strong> sur le compte commun pour honorer les dépenses de la 1ère quinzaine.";
|
||||||
|
} else {
|
||||||
|
$deficit = $q1_expenses_total - $remaining_to_allocate;
|
||||||
|
$remaining_to_allocate = 0;
|
||||||
|
$instructions[] = "💼 <strong>Dès le 1er</strong> : Gardez l'intégralité des apports sur le compte commun.";
|
||||||
|
$instructions[] = "⚠️ <strong>Retrait requis</strong> : Retirez <strong>" . number_format($deficit, 2, ',', ' ') . " €</strong> depuis le Livret A vers le commun pour couvrir la 1ère quinzaine.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Règle 2 : Deuxième quinzaine
|
||||||
|
if ($q2_expenses_total > 0) {
|
||||||
|
if ($remaining_to_allocate >= $q2_expenses_total) {
|
||||||
|
$remaining_to_allocate -= $q2_expenses_total;
|
||||||
|
$instructions[] = "📈 <strong>Dès le 1er</strong> : Placez <strong>" . number_format($q2_expenses_total, 2, ',', ' ') . " €</strong> sur le Livret A pour générer des intérêts sur la 1ère quinzaine.";
|
||||||
|
$instructions[] = "🔄 <strong>Le 16 du mois</strong> : Transférez ces <strong>" . number_format($q2_expenses_total, 2, ',', ' ') . " €</strong> sur le compte commun pour payer la dépense.";
|
||||||
|
} else {
|
||||||
|
if ($remaining_to_allocate > 0) {
|
||||||
|
$instructions[] = "📈 <strong>Dès le 1er</strong> : Placez le reste des apports (<strong>" . number_format($remaining_to_allocate, 2, ',', ' ') . " €</strong>) sur le Livret A.";
|
||||||
|
$instructions[] = "🔄 <strong>Le 16 du mois</strong> : Retirez <strong>" . number_format($q2_expenses_total, 2, ',', ' ') . " €</strong> global du Livret A pour payer la fin de mois.";
|
||||||
|
$remaining_to_allocate = 0;
|
||||||
|
} else {
|
||||||
|
$instructions[] = "🔄 <strong>Le 16 du mois</strong> : Retirez <strong>" . number_format($q2_expenses_total, 2, ',', ' ') . " €</strong> du Livret A vers le commun. <em>Ne le faites pas avant le 16 !</em>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solde résiduel long terme
|
||||||
|
if ($remaining_to_allocate > 0) {
|
||||||
|
$instructions[] = "💰 <strong>Épargne stable</strong> : Placez les <strong>" . number_format($remaining_to_allocate, 2, ',', ' ') . " €</strong> restants sur votre Livret A dès le 1er.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CONSTITUTION DU RENDU HTML FINAL ---
|
||||||
|
$html = '<div style="border-top: 1px dashed var(--border-color); padding-top:0.8rem;">';
|
||||||
|
|
||||||
|
// Titre de l'état du Clearing simplifié et clair
|
||||||
|
$html .= '<h4 style="margin:0 0 0.4rem 0; font-size:0.95rem;">🤝 Résumé des apports et remboursements internes :</h4>';
|
||||||
|
$html .= '<div style="background: var(--bg-main); padding: 8px; border-radius: 6px; margin-bottom: 1rem; border: 1px solid var(--border-color);">';
|
||||||
|
$html .= $contributions_html;
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
$html .= '<h4 style="margin:0 0 0.4rem 0; font-size:0.95rem;">Analyse des dépenses du mois :</h4>';
|
||||||
|
$html .= $detail_html;
|
||||||
|
|
||||||
|
$html .= '<div style="background: var(--bg-main); padding: 8px; border-radius: 6px; border-left: 4px solid var(--accent-color);">';
|
||||||
|
$html .= '<h4 style="margin:0 0 0.5rem 0; color: var(--accent-color); font-size:0.95rem;">📋 Plan d\'action recommandé :</h4>';
|
||||||
|
|
||||||
|
foreach ($instructions as $ins) {
|
||||||
|
$html .= '<p style="margin:0 0 0.4rem 0; font-size: 0.9rem; line-height: 1.4;">' . $ins . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$html .= '</div></div>';
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'html' => $html]);
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Calcul impossible : ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -5,9 +5,30 @@ require __DIR__ . '/../../../../includes/auth.php';
|
|||||||
require __DIR__ . '/../../../../includes/db.php';
|
require __DIR__ . '/../../../../includes/db.php';
|
||||||
require_login();
|
require_login();
|
||||||
|
|
||||||
|
|
||||||
$action = $_POST['action'] ?? '';
|
$action = $_POST['action'] ?? '';
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// LECTURE D'UNE NOTE (AJAX)
|
||||||
|
// =================================================================
|
||||||
|
if ($action === 'get_note') {
|
||||||
|
$noteType = $_POST['note_type'] ?? '';
|
||||||
|
$refId = $_POST['reference_id'] ?? '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = ? AND reference_id = ?");
|
||||||
|
$stmt->execute([$noteType, $refId]);
|
||||||
|
|
||||||
|
// fetchColumn() renvoie false si aucune ligne n'est trouvée, on sécurise avec un coalesce (?? '')
|
||||||
|
$content = $stmt->fetchColumn();
|
||||||
|
$content = $content !== false ? $content : '';
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'content' => $content]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
// 7. SAUVEGARDE D'UNE NOTE GÉNÉRIQUE (pf_notes)
|
// 7. SAUVEGARDE D'UNE NOTE GÉNÉRIQUE (pf_notes)
|
||||||
// =================================================================
|
// =================================================================
|
||||||
@@ -42,7 +63,7 @@ if ($action === 'save_note') {
|
|||||||
|
|
||||||
// 1. MISE A JOUR TABLEAU SALAIRES (AJAX)
|
// 1. MISE A JOUR TABLEAU SALAIRES (AJAX)
|
||||||
if ($action === 'update_salary_config') {
|
if ($action === 'update_salary_config') {
|
||||||
header('Content-Type: application/json'); // On précise JSON ici
|
header('Content-Type: application/json');
|
||||||
$year = $_POST['year'];
|
$year = $_POST['year'];
|
||||||
$person = $_POST['person'];
|
$person = $_POST['person'];
|
||||||
$field = $_POST['field']; // salary, mensualite, etc.
|
$field = $_POST['field']; // salary, mensualite, etc.
|
||||||
@@ -50,7 +71,10 @@ if ($action === 'update_salary_config') {
|
|||||||
|
|
||||||
// Liste des champs autorisés pour éviter les injections
|
// Liste des champs autorisés pour éviter les injections
|
||||||
$allowed = ['salary', 'mensualite', 'frais_func', 'eco_perso', 'eco_family'];
|
$allowed = ['salary', 'mensualite', 'frais_func', 'eco_perso', 'eco_family'];
|
||||||
if (!in_array($field, $allowed)) { echo json_encode(['error'=>'Champ invalide']); exit; }
|
if (!in_array($field, $allowed)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Champ invalide']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO pf_salary_config (year, person, $field) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE $field = VALUES($field)");
|
$stmt = $pdo->prepare("INSERT INTO pf_salary_config (year, person, $field) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE $field = VALUES($field)");
|
||||||
$stmt->execute([$year, $person, $value]);
|
$stmt->execute([$year, $person, $value]);
|
||||||
@@ -61,58 +85,84 @@ if ($action === 'update_salary_config') {
|
|||||||
// 2. MISE A JOUR TABLEAU REPARTITION (AJAX)
|
// 2. MISE A JOUR TABLEAU REPARTITION (AJAX)
|
||||||
if ($action === 'update_allocation') {
|
if ($action === 'update_allocation') {
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
$date = $_POST['month_date'];
|
$date = $_POST['month_date'];
|
||||||
$catId = $_POST['cat_id'];
|
$catId = (int)$_POST['cat_id'];
|
||||||
$person = $_POST['person']; // 'amount_alex' ou 'amount_laia'
|
$personId = (int)$_POST['person_id'];
|
||||||
$value = floatval($_POST['value']);
|
$value = floatval($_POST['value']);
|
||||||
|
|
||||||
|
if ($catId <= 0 || $personId <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Données invalides.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pf_alloc_values (month_date, cat_id, person_id, amount)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE amount = VALUES(amount)");
|
||||||
|
$stmt->execute([$date, $catId, $personId, $value]);
|
||||||
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO pf_alloc_values (month_date, cat_id, $person) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE $person = VALUES($person)");
|
|
||||||
$stmt->execute([$date, $catId, $value]);
|
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. GESTION DES CATEGORIES (Ajout)
|
// 3. GESTION DES CATEGORIES (Ajout)
|
||||||
if ($action === 'add_category') {
|
if ($action === 'add_category') {
|
||||||
$name = trim($_POST['name']);
|
try {
|
||||||
$target = trim($_POST['target']);
|
$name = trim($_POST['name']);
|
||||||
$holiday_id = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
|
$transfer_dest = trim($_POST['transfer_dest'] ?? '');
|
||||||
|
$target = floatval($_POST['target'] ?? 0);
|
||||||
|
$holiday_id = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
|
||||||
|
|
||||||
if (!empty($name)) {
|
if (!empty($name)) {
|
||||||
$stmt = $pdo->prepare("INSERT INTO pf_alloc_categories (name, target, holiday_id) VALUES (?, ?, ?)");
|
$stmt = $pdo->prepare("INSERT INTO pf_alloc_categories (name, target, transfer_dest, holiday_id) VALUES (?, ?, ?, ?)");
|
||||||
$stmt->execute([$name, $target, $holiday_id]);
|
$stmt->execute([$name, $target, $transfer_dest, $holiday_id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- NOUVEAU : Réponse AJAX ---
|
if (isset($_POST['ajax'])) {
|
||||||
if (isset($_POST['ajax'])) {
|
header('Content-Type: application/json');
|
||||||
header('Content-Type: application/json');
|
echo json_encode(['success' => true]);
|
||||||
echo json_encode(['success' => true]);
|
exit;
|
||||||
|
}
|
||||||
|
header("Location: " . $_SERVER['HTTP_REFERER']);
|
||||||
exit;
|
exit;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if (isset($_POST['ajax'])) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
die($e->getMessage());
|
||||||
}
|
}
|
||||||
header("Location: " . $_SERVER['HTTP_REFERER']);
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. MODIFICATION D'UNE CATEGORIE
|
// 4. MODIFICATION D'UNE CATEGORIE
|
||||||
if ($action === 'update_category') {
|
if ($action === 'update_category') {
|
||||||
$id = (int)$_POST['cat_id'];
|
try {
|
||||||
$name = trim($_POST['name']);
|
$id = (int)$_POST['cat_id'];
|
||||||
$target = trim($_POST['target']);
|
$name = trim($_POST['name']);
|
||||||
$holiday_id = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
|
$transfer_dest = trim($_POST['transfer_dest'] ?? '');
|
||||||
|
$target = floatval($_POST['target'] ?? 0);
|
||||||
|
$holiday_id = !empty($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : null;
|
||||||
|
|
||||||
if ($id > 0 && !empty($name)) {
|
if ($id > 0 && !empty($name)) {
|
||||||
$stmt = $pdo->prepare("UPDATE pf_alloc_categories SET name = ?, target = ?, holiday_id = ? WHERE id = ?");
|
$stmt = $pdo->prepare("UPDATE pf_alloc_categories SET name = ?, target = ?, transfer_dest = ?, holiday_id = ? WHERE id = ?");
|
||||||
$stmt->execute([$name, $target, $holiday_id, $id]);
|
$stmt->execute([$name, $target, $transfer_dest, $holiday_id, $id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- NOUVEAU : Réponse AJAX ---
|
if (isset($_POST['ajax'])) {
|
||||||
if (isset($_POST['ajax'])) {
|
header('Content-Type: application/json');
|
||||||
header('Content-Type: application/json');
|
echo json_encode(['success' => true]);
|
||||||
echo json_encode(['success' => true]);
|
exit;
|
||||||
|
}
|
||||||
|
header("Location: " . $_SERVER['HTTP_REFERER']);
|
||||||
exit;
|
exit;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if (isset($_POST['ajax'])) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
die($e->getMessage());
|
||||||
}
|
}
|
||||||
header("Location: " . $_SERVER['HTTP_REFERER']);
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. SUPPRESSION CATEGORIE
|
// 5. SUPPRESSION CATEGORIE
|
||||||
@@ -136,36 +186,64 @@ if ($action === 'delete_category') {
|
|||||||
// 6. VALIDATION DES VIREMENTS (Complex Business Logic)
|
// 6. VALIDATION DES VIREMENTS (Complex Business Logic)
|
||||||
if ($action === 'validate_transfers') {
|
if ($action === 'validate_transfers') {
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
$person = $_POST['person'];
|
$personId = (int)$_POST['person_id']; // Reçoit l'ID parent
|
||||||
$monthDate = $_POST['month_date'];
|
$monthDate = $_POST['month_date'];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
$stmtP = $pdo->prepare("SELECT name FROM pf_people WHERE id = ?");
|
||||||
|
$stmtP->execute([$personId]);
|
||||||
|
$dbPersonName = $stmtP->fetchColumn();
|
||||||
|
if (!$dbPersonName) throw new Exception("Parent introuvable.");
|
||||||
|
|
||||||
|
$stmtAcc = $pdo->query("SELECT a.name as acc_name, p.name as owner_name FROM pf_bank_accounts a LEFT JOIN pf_people p ON a.owner_person_id = p.id");
|
||||||
|
$bankAccountsMap = [];
|
||||||
|
|
||||||
|
// Fallback intelligent : on récupère tous les prénoms de la famille
|
||||||
|
$familyNames = $pdo->query("SELECT name FROM pf_people")->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
while ($row = $stmtAcc->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
$owner = $row['owner_name'];
|
||||||
|
if (!$owner) {
|
||||||
|
// Si pas de propriétaire explicite, on cherche un prénom dans le nom du compte (ex: "Livret A Alex")
|
||||||
|
foreach ($familyNames as $pName) {
|
||||||
|
if (stripos($row['acc_name'], $pName) !== false) {
|
||||||
|
$owner = $pName;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$bankAccountsMap['vers ' . $row['acc_name']] = $owner;
|
||||||
|
}
|
||||||
|
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
SELECT v.*, c.name as cat_name, c.target, c.holiday_id
|
SELECT v.amount, c.name as cat_name, c.transfer_dest, c.holiday_id
|
||||||
FROM pf_alloc_values v
|
FROM pf_alloc_values v
|
||||||
JOIN pf_alloc_categories c ON v.cat_id = c.id
|
JOIN pf_alloc_categories c ON v.cat_id = c.id
|
||||||
WHERE v.month_date = ?
|
WHERE v.month_date = ? AND v.person_id = ?
|
||||||
");
|
");
|
||||||
$stmt->execute([$monthDate]);
|
$stmt->execute([$monthDate, $personId]);
|
||||||
$budgetLines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$budgetLines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$transfersToDo = [];
|
$transfersToDo = [];
|
||||||
|
|
||||||
foreach ($budgetLines as $line) {
|
foreach ($budgetLines as $line) {
|
||||||
$amount = ($person === 'Alex') ? $line['amount_alex'] : $line['amount_laia'];
|
$amount = (float)$line['amount'];
|
||||||
if ($amount <= 0) continue;
|
if ($amount <= 0) continue;
|
||||||
|
|
||||||
$target = trim($line['target']);
|
$dest = trim($line['transfer_dest']);
|
||||||
$catName = trim($line['cat_name']);
|
$catName = trim($line['cat_name']);
|
||||||
$holidayId = $line['holiday_id'];
|
$holidayId = $line['holiday_id'];
|
||||||
|
|
||||||
$targetOwner = null;
|
$targetOwner = null;
|
||||||
if ($target === 'vers L.Perso') { $targetOwner = $person; }
|
if (isset($bankAccountsMap[$dest])) {
|
||||||
elseif ($target === 'vers L.Pol') { $targetOwner = 'Pol'; }
|
$targetOwner = $bankAccountsMap[$dest];
|
||||||
elseif ($target === 'vers L.Pep') { $targetOwner = 'Pep'; }
|
if (!$targetOwner) continue; // Compte commun sans propriétaire
|
||||||
elseif ($target === 'vers commune') { continue; }
|
} elseif ($dest === 'vers L.Perso') { $targetOwner = $dbPersonName; }
|
||||||
|
elseif ($dest === 'vers L.Pol') { $targetOwner = 'Pol'; }
|
||||||
|
elseif ($dest === 'vers L.Pep') { $targetOwner = 'Pep'; }
|
||||||
|
elseif ($dest === 'vers commune') { continue; }
|
||||||
|
|
||||||
if ($targetOwner) {
|
if ($targetOwner) {
|
||||||
if (!isset($transfersToDo[$targetOwner])) {
|
if (!isset($transfersToDo[$targetOwner])) {
|
||||||
@@ -181,7 +259,6 @@ if ($action === 'validate_transfers') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($transfersToDo as $owner => $data) {
|
foreach ($transfersToDo as $owner => $data) {
|
||||||
// A. VERIFIER EXISTENCE (Inchangé)
|
|
||||||
$stmtCheck = $pdo->prepare("SELECT COUNT(*) FROM pf_savings WHERE owner = ? AND month_date = ? AND category = 'TOTAL_BANQUE'");
|
$stmtCheck = $pdo->prepare("SELECT COUNT(*) FROM pf_savings WHERE owner = ? AND month_date = ? AND category = 'TOTAL_BANQUE'");
|
||||||
$stmtCheck->execute([$owner, $monthDate]);
|
$stmtCheck->execute([$owner, $monthDate]);
|
||||||
$exists = $stmtCheck->fetchColumn() > 0;
|
$exists = $stmtCheck->fetchColumn() > 0;
|
||||||
@@ -200,54 +277,38 @@ if ($action === 'validate_transfers') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// B. UPDATE TOTAL (Inchangé)
|
|
||||||
$stmtUpdTotal = $pdo->prepare("UPDATE pf_savings SET amount = amount + ? WHERE owner = ? AND month_date = ? AND category = 'TOTAL_BANQUE'");
|
$stmtUpdTotal = $pdo->prepare("UPDATE pf_savings SET amount = amount + ? WHERE owner = ? AND month_date = ? AND category = 'TOTAL_BANQUE'");
|
||||||
$stmtUpdTotal->execute([$data['total_add'], $owner, $monthDate]);
|
$stmtUpdTotal->execute([$data['total_add'], $owner, $monthDate]);
|
||||||
|
|
||||||
// C. UPDATE CATÉGORIES (Modifié pour gérer le holiday_id)
|
|
||||||
foreach ($data['cats'] as $catName => $catInfo) {
|
foreach ($data['cats'] as $catName => $catInfo) {
|
||||||
$catAmount = $catInfo['amount'];
|
$catAmount = $catInfo['amount'];
|
||||||
$catHolidayId = $catInfo['holiday_id']; // NOUVEAU
|
$catHolidayId = $catInfo['holiday_id'];
|
||||||
|
|
||||||
if ($catName === 'Eco Alex' || $catName === 'Eco Laia') { continue; }
|
if (preg_match('/^(Eco\s|Eco P\d|Livret)/i', $catName)) { continue; }
|
||||||
|
|
||||||
$stmtCheckCat = $pdo->prepare("SELECT id FROM pf_savings WHERE owner = ? AND month_date = ? AND category = ?");
|
$stmtCheckCat = $pdo->prepare("SELECT id FROM pf_savings WHERE owner = ? AND month_date = ? AND category = ?");
|
||||||
$stmtCheckCat->execute([$owner, $monthDate, $catName]);
|
$stmtCheckCat->execute([$owner, $monthDate, $catName]);
|
||||||
$catId = $stmtCheckCat->fetchColumn();
|
$catId = $stmtCheckCat->fetchColumn();
|
||||||
|
|
||||||
if ($catId) {
|
if ($catId) {
|
||||||
// Update : On actualise aussi le holiday_id au cas où il aurait changé
|
|
||||||
$stmtUpdateCat = $pdo->prepare("UPDATE pf_savings SET amount = amount + ?, holiday_id = ? WHERE id = ?");
|
$stmtUpdateCat = $pdo->prepare("UPDATE pf_savings SET amount = amount + ?, holiday_id = ? WHERE id = ?");
|
||||||
$stmtUpdateCat->execute([$catAmount, $catHolidayId, $catId]);
|
$stmtUpdateCat->execute([$catAmount, $catHolidayId, $catId]);
|
||||||
} else {
|
} else {
|
||||||
// Insert
|
|
||||||
$stmtInsertCat = $pdo->prepare("INSERT INTO pf_savings (month_date, owner, category, amount, holiday_id) VALUES (?, ?, ?, ?, ?)");
|
$stmtInsertCat = $pdo->prepare("INSERT INTO pf_savings (month_date, owner, category, amount, holiday_id) VALUES (?, ?, ?, ?, ?)");
|
||||||
$stmtInsertCat->execute([$monthDate, $owner, $catName, $catAmount, $catHolidayId]);
|
$stmtInsertCat->execute([$monthDate, $owner, $catName, $catAmount, $catHolidayId]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. ENREGISTRER LA VALIDATION (Mise à jour table existante)
|
|
||||||
|
|
||||||
// a. Trouver l'ID de la catégorie système
|
|
||||||
$stmtSys = $pdo->prepare("SELECT id FROM pf_alloc_categories WHERE name = 'SYSTEM_VALIDATION' LIMIT 1");
|
$stmtSys = $pdo->prepare("SELECT id FROM pf_alloc_categories WHERE name = 'SYSTEM_VALIDATION' LIMIT 1");
|
||||||
$stmtSys->execute();
|
$stmtSys->execute();
|
||||||
$sysCatId = $stmtSys->fetchColumn();
|
$sysCatId = $stmtSys->fetchColumn();
|
||||||
|
|
||||||
if ($sysCatId) {
|
if ($sysCatId) {
|
||||||
|
$stmtVal = $pdo->prepare("INSERT INTO pf_alloc_values (month_date, cat_id, person_id, amount)
|
||||||
if ($person === 'Alex') {
|
VALUES (?, ?, ?, 1)
|
||||||
$sql = "INSERT INTO pf_alloc_values (month_date, cat_id, amount_alex, amount_laia)
|
ON DUPLICATE KEY UPDATE amount = 1");
|
||||||
VALUES (?, ?, 1, 0)
|
$stmtVal->execute([$monthDate, $sysCatId, $personId]);
|
||||||
ON DUPLICATE KEY UPDATE amount_alex = 1";
|
|
||||||
} else {
|
|
||||||
$sql = "INSERT INTO pf_alloc_values (month_date, cat_id, amount_alex, amount_laia)
|
|
||||||
VALUES (?, ?, 0, 1)
|
|
||||||
ON DUPLICATE KEY UPDATE amount_laia = 1";
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmtVal = $pdo->prepare($sql);
|
|
||||||
$stmtVal->execute([$monthDate, $sysCatId]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
@@ -258,6 +319,91 @@ if ($action === 'validate_transfers') {
|
|||||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
}
|
}
|
||||||
exit;
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// 7. GESTION DES AVANCES / TRICOUNT (pf_advances)
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
if ($action === 'save_advance') {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
try {
|
||||||
|
$payer = trim($_POST['payer'] ?? '');
|
||||||
|
$advance_date = $_POST['advance_date'] ?? date('Y-m-d');
|
||||||
|
$description = trim($_POST['description'] ?? '');
|
||||||
|
$amount = abs((float)($_POST['amount'] ?? 0));
|
||||||
|
$from_savings = isset($_POST['from_savings']) ? 1 : 0;
|
||||||
|
|
||||||
|
if (empty($payer) || empty($description) || $amount <= 0) {
|
||||||
|
throw new Exception("Champs obligatoires manquants ou invalides.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pf_advances (advance_date, payer, description, amount, from_savings) VALUES (?, ?, ?, ?, ?)");
|
||||||
|
$stmt->execute([$advance_date, $payer, $description, $amount, $from_savings]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'update_advance') {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
try {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$payer = trim($_POST['payer'] ?? '');
|
||||||
|
$advance_date = $_POST['advance_date'] ?? date('Y-m-d');
|
||||||
|
$description = trim($_POST['description'] ?? '');
|
||||||
|
$amount = abs((float)($_POST['amount'] ?? 0));
|
||||||
|
$from_savings = isset($_POST['from_savings']) ? 1 : 0;
|
||||||
|
|
||||||
|
if ($id <= 0 || empty($payer) || empty($description) || $amount <= 0) {
|
||||||
|
throw new Exception("Paramètres invalides fournis.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_advances SET advance_date = ?, payer = ?, description = ?, amount = ?, from_savings = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$advance_date, $payer, $description, $amount, $from_savings, $id]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'resolve_advance') {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
try {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) throw new Exception("ID invalide fourni.");
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_advances SET is_resolved = 1 WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'delete_advance') {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
try {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) throw new Exception("ID invalide.");
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM pf_advances WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
@@ -3,23 +3,31 @@ require __DIR__ . '/../../../../includes/auth.php';
|
|||||||
require __DIR__ . '/../../../../includes/db.php';
|
require __DIR__ . '/../../../../includes/db.php';
|
||||||
require_login();
|
require_login();
|
||||||
|
|
||||||
|
// 1. Déclaration propre des variables d'entrée
|
||||||
|
$action = $_POST['action'] ?? '';
|
||||||
|
$isAjax = !empty($_POST['ajax']);
|
||||||
|
|
||||||
|
// Si c'est de l'AJAX, on force les en-têtes HTTP pour garantir du JSON
|
||||||
|
if ($isAjax) {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
// MISE À JOUR D'UNE CELLULE EN DIRECT (AJAX)
|
// MISE À JOUR D'UNE CELLULE EN DIRECT (AJAX)
|
||||||
// =================================================================
|
// =================================================================
|
||||||
if ($action === 'update_single_entry') {
|
if ($action === 'update_single_entry') {
|
||||||
header('Content-Type: application/json');
|
$month = $_POST['month_date'] ?? '';
|
||||||
$month = $_POST['month_date'];
|
$cat = $_POST['category'] ?? '';
|
||||||
$cat = $_POST['category'];
|
$owner = $_POST['owner'] ?? '';
|
||||||
$owner = $_POST['owner'];
|
$amount = (float)($_POST['amount'] ?? 0);
|
||||||
$amount = (float)$_POST['amount'];
|
$forceInsert = !empty($_POST['force_insert']);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($amount == 0 && $cat !== 'TOTAL_BANQUE') {
|
// On supprime la ligne si elle est à 0, SAUF si on demande explicitement son initialisation (force_insert)
|
||||||
// Si on met à 0 une ligne (autre que le total), on supprime l'entrée pour garder la base propre
|
if ($amount == 0 && $cat !== 'TOTAL_BANQUE' && !$forceInsert) {
|
||||||
$stmt = $pdo->prepare("DELETE FROM pf_savings WHERE month_date=? AND owner=? AND category=?");
|
$stmt = $pdo->prepare("DELETE FROM pf_savings WHERE month_date=? AND owner=? AND category=?");
|
||||||
$stmt->execute([$month, $owner, $cat]);
|
$stmt->execute([$month, $owner, $cat]);
|
||||||
} else {
|
} else {
|
||||||
// Sinon on insère ou on met à jour
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO pf_savings (month_date, owner, category, amount) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE amount = VALUES(amount)");
|
$stmt = $pdo->prepare("INSERT INTO pf_savings (month_date, owner, category, amount) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE amount = VALUES(amount)");
|
||||||
$stmt->execute([$month, $owner, $cat, $amount]);
|
$stmt->execute([$month, $owner, $cat, $amount]);
|
||||||
}
|
}
|
||||||
@@ -31,10 +39,10 @@ if ($action === 'update_single_entry') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- ACTION : SUPPRESSION D'UNE ENTRÉE UNIQUE ---
|
// --- ACTION : SUPPRESSION D'UNE ENTRÉE UNIQUE ---
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete_entry') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'delete_entry') {
|
||||||
$owner = $_POST['owner'];
|
$owner = $_POST['owner'] ?? '';
|
||||||
$date = $_POST['month_date'];
|
$date = $_POST['month_date'] ?? '';
|
||||||
$cat = $_POST['category'];
|
$cat = $_POST['category'] ?? '';
|
||||||
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM pf_savings WHERE owner = ? AND month_date = ? AND category = ?");
|
$stmt = $pdo->prepare("DELETE FROM pf_savings WHERE owner = ? AND month_date = ? AND category = ?");
|
||||||
$stmt->execute([$owner, $date, $cat]);
|
$stmt->execute([$owner, $date, $cat]);
|
||||||
@@ -44,11 +52,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- ACTION : DUPLICATION DE MOIS ---
|
// --- ACTION : DUPLICATION DE MOIS ---
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'duplicate_month') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'duplicate_month') {
|
||||||
$owner = $_POST['owner'];
|
$owner = $_POST['owner'] ?? '';
|
||||||
$sourceDate = $_POST['source_date'];
|
$sourceDate = $_POST['source_date'] ?? '';
|
||||||
$targetDate = $_POST['target_date'];
|
$targetDate = $_POST['target_date'] ?? '';
|
||||||
$newTotal = floatval($_POST['new_total']);
|
$newTotal = floatval($_POST['new_total'] ?? 0);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
@@ -78,16 +86,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'dupli
|
|||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$pdo->rollBack();
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
}
|
}
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ACTION : SUPPRESSION GLOBALE D'UN MOIS ---
|
// --- ACTION : SUPPRESSION GLOBALE D'UN MOIS ---
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete_month_global') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'delete_month_global') {
|
||||||
$owner = $_POST['owner'];
|
$owner = $_POST['owner'] ?? '';
|
||||||
$date = $_POST['month_date'];
|
$date = $_POST['month_date'] ?? '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$stmt = $pdo->prepare("DELETE FROM pf_savings WHERE owner = ? AND month_date = ?");
|
$stmt = $pdo->prepare("DELETE FROM pf_savings WHERE owner = ? AND month_date = ?");
|
||||||
@@ -101,18 +109,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- ACTION : SAUVEGARDE CLASSIQUE (MODALE) ---
|
// --- ACTION : SAUVEGARDE CLASSIQUE (MODALE) ---
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
// Cette action s'exécute si c'est un POST général sans action précise (le form HTML) ou l'action explicitement nommée.
|
||||||
$owner = $_POST['owner'];
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && in_array($action, ['save_modal', ''])) {
|
||||||
|
$owner = $_POST['owner'] ?? '';
|
||||||
// CORRECTION ICI : On récupère l'onglet de redirection (Nens, Alex ou Laia)
|
|
||||||
$redirectTab = $_POST['redirect_tab'] ?? $owner;
|
$redirectTab = $_POST['redirect_tab'] ?? $owner;
|
||||||
|
|
||||||
$dateInput = $_POST['month_date'];
|
$dateInput = $_POST['month_date'] ?? '';
|
||||||
$dateObj = new DateTime($dateInput);
|
|
||||||
$monthDate = $dateObj->format('Y-m-01');
|
|
||||||
$values = $_POST['values'] ?? []; // Tableau généré par nos champs JS: values[Catégorie]
|
$values = $_POST['values'] ?? []; // Tableau généré par nos champs JS: values[Catégorie]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
$dateObj = new DateTime($dateInput);
|
||||||
|
$monthDate = $dateObj->format('Y-m-01');
|
||||||
|
|
||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
// On supprime d'abord les anciennes données du mois pour cet utilisateur
|
// On supprime d'abord les anciennes données du mois pour cet utilisateur
|
||||||
@@ -130,11 +138,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
}
|
}
|
||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
|
|
||||||
// Redirection vers le bon onglet
|
// On répond proprement selon le contexte (AJAX ou Fallback classique)
|
||||||
header("Location: /budget.php?tab=epargne&owner=" . urlencode($redirectTab));
|
if ($isAjax) {
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} else {
|
||||||
|
header("Location: /budget.php?tab=epargne&owner=" . urlencode($redirectTab));
|
||||||
|
}
|
||||||
exit;
|
exit;
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$pdo->rollBack();
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
die("Erreur : " . $e->getMessage());
|
|
||||||
|
if ($isAjax) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
} else {
|
||||||
|
die("Erreur : " . $e->getMessage());
|
||||||
|
}
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sécurité finale : Si l'action n'est pas reconnue
|
||||||
|
if ($isAjax) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Action inconnue']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
<?php
|
||||||
|
require __DIR__ . '/../../../../includes/auth.php';
|
||||||
|
require __DIR__ . '/../../../../includes/db.php';
|
||||||
|
require_once __DIR__ . '/../../../../includes/i18n.php';
|
||||||
|
require_login();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
$action = $_GET['action'] ?? $_POST['action'] ?? '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($action === 'get_all') {
|
||||||
|
// 1. Comptes bancaires avec le nom du propriétaire lié (pf_people)
|
||||||
|
$accounts = $pdo->query("
|
||||||
|
SELECT a.*, p.name as owner_name
|
||||||
|
FROM pf_bank_accounts a
|
||||||
|
LEFT JOIN pf_people p ON a.owner_person_id = p.id
|
||||||
|
ORDER BY a.is_default DESC, a.name ASC
|
||||||
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// 2. Catégories
|
||||||
|
$categories = $pdo->query("SELECT * FROM pf_budget_categories ORDER BY type ASC, label ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// 3. Règles d'import
|
||||||
|
$rules = $pdo->query("
|
||||||
|
SELECT r.*, c.label as cat_label
|
||||||
|
FROM pf_import_rules r
|
||||||
|
LEFT JOIN pf_budget_categories c ON r.category COLLATE utf8mb4_unicode_ci = c.code COLLATE utf8mb4_unicode_ci
|
||||||
|
ORDER BY r.keyword ASC
|
||||||
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// 4. Salaires (Année en cours)
|
||||||
|
$currentYear = (int)date('Y');
|
||||||
|
$salaries = $pdo->query("
|
||||||
|
SELECT p.name as person,
|
||||||
|
COALESCE(s.salary, 0) as salary,
|
||||||
|
COALESCE(s.mensualite, 0) as mensualite,
|
||||||
|
s.id as id
|
||||||
|
FROM pf_people p
|
||||||
|
LEFT JOIN pf_salary_config s ON p.name = s.person AND s.year = $currentYear
|
||||||
|
WHERE p.role NOT IN ('enfant', 'nounou') AND p.is_active = 1
|
||||||
|
ORDER BY p.name ASC
|
||||||
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// 5. Paramètres Foyer (Devise + Mapping CSV)
|
||||||
|
$foyerData = $pdo->query("SELECT currency, csv_mapping FROM pf_foyer_settings LIMIT 1")->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$currencySetting = $foyerData['currency'] ?? '€';
|
||||||
|
$csvMapping = !empty($foyerData['csv_mapping']) ? json_decode($foyerData['csv_mapping'], true) : null;
|
||||||
|
|
||||||
|
// --- NOUVEAU : Récupération des paramètres dynamiques ---
|
||||||
|
$stmtSettings = $pdo->query("SELECT setting_key, setting_value FROM pf_settings WHERE module = 'budget'");
|
||||||
|
$budgetSettings = $stmtSettings->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
|
|
||||||
|
// 6. Liste des membres (pour assigner un compte bancaire)
|
||||||
|
$people = $pdo->query("SELECT id, name FROM pf_people WHERE is_active = 1 ORDER BY name ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'accounts' => $accounts,
|
||||||
|
'categories' => $categories,
|
||||||
|
'rules' => $rules,
|
||||||
|
'salaries' => $salaries,
|
||||||
|
'people' => $people,
|
||||||
|
'year' => $currentYear,
|
||||||
|
'currency' => $currencySetting,
|
||||||
|
'csv_mapping' => $csvMapping,
|
||||||
|
'budget_settings' => $budgetSettings
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GESTION DES COMPTES BANCAIRES ---
|
||||||
|
if ($action === 'add_account') {
|
||||||
|
$name = trim($_POST['name'] ?? '');
|
||||||
|
$type = $_POST['type'] ?? 'checking';
|
||||||
|
$owner_id = !empty($_POST['owner_person_id']) ? (int)$_POST['owner_person_id'] : null;
|
||||||
|
if (empty($name)) throw new Exception("Le nom du compte est obligatoire.");
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pf_bank_accounts (name, account_type, is_default, owner_person_id) VALUES (?, ?, 0, ?)");
|
||||||
|
$stmt->execute([$name, $type, $owner_id]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'delete_account') {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$count = $pdo->query("SELECT COUNT(*) FROM pf_bank_accounts")->fetchColumn();
|
||||||
|
if ($count <= 1) throw new Exception("Impossible de supprimer le dernier compte.");
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM pf_bank_accounts WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GESTION DES CATÉGORIES ---
|
||||||
|
if ($action === 'add_category') {
|
||||||
|
$code = strtoupper(trim($_POST['code'] ?? ''));
|
||||||
|
$label = trim($_POST['label'] ?? '');
|
||||||
|
$type = $_POST['type'] ?? 'Expense';
|
||||||
|
$color = $_POST['color'] ?? '#cccccc';
|
||||||
|
$icon = trim($_POST['icon'] ?? '📌');
|
||||||
|
if (empty($code) || empty($label)) throw new Exception("Le code et le libellé sont obligatoires.");
|
||||||
|
$check = $pdo->prepare("SELECT COUNT(*) FROM pf_budget_categories WHERE code = ?");
|
||||||
|
$check->execute([$code]);
|
||||||
|
if ($check->fetchColumn() > 0) throw new Exception("Ce code de catégorie existe déjà.");
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pf_budget_categories (code, label, type, color, icon) VALUES (?, ?, ?, ?, ?)");
|
||||||
|
$stmt->execute([$code, $label, $type, $color, $icon]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'delete_category') {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
|
$stmtCheck = $pdo->prepare("SELECT code FROM pf_budget_categories WHERE id = ?");
|
||||||
|
$stmtCheck->execute([$id]);
|
||||||
|
$catCode = $stmtCheck->fetchColumn();
|
||||||
|
|
||||||
|
if ($catCode && in_array(strtoupper($catCode), ['FIXED', 'INCOME'])) {
|
||||||
|
throw new Exception(tr('bud_err_delete_system_cat'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM pf_budget_categories WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GESTION DES RÈGLES D'IMPORT ---
|
||||||
|
if ($action === 'add_rule') {
|
||||||
|
$keyword = strtoupper(trim($_POST['keyword'] ?? ''));
|
||||||
|
$category = trim($_POST['category'] ?? '');
|
||||||
|
if (empty($keyword) || empty($category)) throw new Exception("Le mot-clé et la catégorie sont obligatoires.");
|
||||||
|
$check = $pdo->prepare("SELECT COUNT(*) FROM pf_import_rules WHERE keyword = ?");
|
||||||
|
$check->execute([$keyword]);
|
||||||
|
if ($check->fetchColumn() > 0) throw new Exception("Une règle pour ce mot-clé existe déjà.");
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category) VALUES (?, ?)");
|
||||||
|
$stmt->execute([$keyword, $category]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'delete_rule') {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM pf_import_rules WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GESTION DES SALAIRES ---
|
||||||
|
if ($action === 'save_salary') {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$salary = (float)($_POST['salary'] ?? 0);
|
||||||
|
$mensualite = (float)($_POST['mensualite'] ?? 0);
|
||||||
|
if ($id <= 0) throw new Exception("ID de configuration de salaire invalide.");
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_salary_config SET salary = ?, mensualite = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$salary, $mensualite, $id]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GESTION DE LA DEVISE GLOBALE ---
|
||||||
|
if ($action === 'save_currency') {
|
||||||
|
$currency = trim($_POST['currency'] ?? '€');
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_foyer_settings SET currency = ?");
|
||||||
|
$stmt->execute([$currency]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GESTION DU FORMAT CSV ---
|
||||||
|
if ($action === 'save_csv_mapping') {
|
||||||
|
$mapping = [
|
||||||
|
'delimiter' => $_POST['csv_delimiter'] ?? ';',
|
||||||
|
'date_format' => $_POST['csv_date_format'] ?? 'd/m/Y',
|
||||||
|
'col_date' => (int)($_POST['csv_col_date'] ?? 0),
|
||||||
|
'col_label' => (int)($_POST['csv_col_label'] ?? 1),
|
||||||
|
'amount_type' => $_POST['csv_amount_type'] ?? 'single',
|
||||||
|
'col_debit' => (int)($_POST['csv_col_debit'] ?? 8),
|
||||||
|
'col_credit' => (int)($_POST['csv_col_credit'] ?? 9),
|
||||||
|
'col_ref' => (int)($_POST['csv_col_ref'] ?? 3)
|
||||||
|
];
|
||||||
|
$jsonContent = json_encode($mapping);
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_foyer_settings SET csv_mapping = ?");
|
||||||
|
$stmt->execute([$jsonContent]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAUVEGARDE DU NOUVEL ORDRE DES CATÉGORIES
|
||||||
|
if (isset($_POST['action']) && $_POST['action'] === 'save_categories_order') {
|
||||||
|
$ids = json_decode($_POST['order'], true);
|
||||||
|
if (is_array($ids)) {
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_budget_categories SET sort_order = ? WHERE id = ?");
|
||||||
|
foreach ($ids as $index => $id) {
|
||||||
|
$stmt->execute([$index, (int)$id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GESTION DES ESTIMATIONS DYNAMIQUES ---
|
||||||
|
if ($action === 'save_dynamic_estimates') {
|
||||||
|
$keys = [
|
||||||
|
'budget_nanny_fixed', 'budget_nanny_daily', 'budget_nanny_aid', 'budget_nanny_cesu_avg',
|
||||||
|
'budget_school_meal', 'budget_school_aftercare', 'budget_school_fullday'
|
||||||
|
];
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pf_settings (setting_key, setting_value, module) VALUES (?, ?, 'budget') ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value), updated_at = NOW()");
|
||||||
|
|
||||||
|
foreach ($keys as $k) {
|
||||||
|
if (isset($_POST[$k])) {
|
||||||
|
$stmt->execute([$k, $_POST[$k]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception("Action API inconnue.");
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
File diff suppressed because it is too large.
Load diff
+260
-203
@@ -1,9 +1,31 @@
|
|||||||
<?php
|
<?php
|
||||||
// modules/budget/views/epargne.php
|
// modules/budget/views/epargne.php
|
||||||
|
|
||||||
$requestedOwner = $_GET['owner'] ?? 'Nens';
|
// --- 🧠 CONFIGURATION AGNOSTIQUE DES MEMBRES (MULTI-TENANT) ---
|
||||||
$ownersToDisplay = ($requestedOwner === 'Nens') ? ['Pol', 'Pep'] : [$requestedOwner];
|
$stmtPeople = $pdo->query("SELECT name, role, color FROM pf_people ORDER BY id ASC");
|
||||||
|
$familyParents = [];
|
||||||
|
$familyKids = [];
|
||||||
|
$personColors = [];
|
||||||
|
|
||||||
|
while ($row = $stmtPeople->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
$role = strtolower(trim($row['role'] ?? ''));
|
||||||
|
$personColors[$row['name']] = $row['color'] ?? '#3b82f6';
|
||||||
|
|
||||||
|
if ($role === 'parent') {
|
||||||
|
$familyParents[] = $row['name'];
|
||||||
|
} elseif ($role === 'nounou' || $role === 'helper') {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
$familyKids[] = $row['name'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($familyParents)) $familyParents = ['Parent 1', 'Parent 2'];
|
||||||
|
|
||||||
|
$requestedOwner = $_GET['owner'] ?? (!empty($familyKids) ? 'KIDS' : ($familyParents[0] ?? 'KIDS'));
|
||||||
|
$ownersToDisplay = ($requestedOwner === 'KIDS') ? $familyKids : [$requestedOwner];
|
||||||
|
|
||||||
|
// --- RÉCUPÉRATION CONFIGURATION DES MOIS ---
|
||||||
$cycleConfigs = [];
|
$cycleConfigs = [];
|
||||||
$stmtNotes = $pdo->query("SELECT reference_id, content FROM pf_notes WHERE note_type = 'month_config'");
|
$stmtNotes = $pdo->query("SELECT reference_id, content FROM pf_notes WHERE note_type = 'month_config'");
|
||||||
while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
|
while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
|
||||||
@@ -14,7 +36,6 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupération sécurisée du nom des mois (utilise les clés globales existantes)
|
|
||||||
function getMonthName($dateString) {
|
function getMonthName($dateString) {
|
||||||
$m = date('m', strtotime($dateString));
|
$m = date('m', strtotime($dateString));
|
||||||
$y = date('Y', strtotime($dateString));
|
$y = date('Y', strtotime($dateString));
|
||||||
@@ -25,13 +46,21 @@ function getMonthName($dateString) {
|
|||||||
<div class="budget-view">
|
<div class="budget-view">
|
||||||
<div class="view-header">
|
<div class="view-header">
|
||||||
<div class="owner-tabs">
|
<div class="owner-tabs">
|
||||||
<a href="?tab=epargne&owner=Alex" class="owner-tab <?= $requestedOwner === 'Alex' ? 'active' : '' ?>">Alex</a>
|
<?php if (!empty($familyKids)): ?>
|
||||||
<a href="?tab=epargne&owner=Laia" class="owner-tab <?= $requestedOwner === 'Laia' ? 'active' : '' ?>">Laia</a>
|
<a href="?tab=epargne&owner=KIDS" class="owner-tab <?= $requestedOwner === 'KIDS' ? 'active' : '' ?>">
|
||||||
<a href="?tab=epargne&owner=Nens" class="owner-tab <?= $requestedOwner === 'Nens' ? 'active' : '' ?>">Nens 👶</a>
|
<?= tr('budget_tab_kids') ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php foreach ($familyParents as $p): ?>
|
||||||
|
<a href="?tab=epargne&owner=<?= urlencode($p) ?>" class="owner-tab <?= $requestedOwner === $p ? 'active' : '' ?>">
|
||||||
|
<?= htmlspecialchars($p) ?>
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php foreach ($ownersToDisplay as $currentOwner):
|
<?php foreach ($ownersToDisplay as $index => $currentOwner):
|
||||||
$stmt = $pdo->prepare("SELECT month_date, category, amount FROM pf_savings WHERE owner = ? ORDER BY month_date DESC");
|
$stmt = $pdo->prepare("SELECT month_date, category, amount FROM pf_savings WHERE owner = ? ORDER BY month_date DESC");
|
||||||
$stmt->execute([$currentOwner]);
|
$stmt->execute([$currentOwner]);
|
||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
@@ -51,183 +80,174 @@ function getMonthName($dateString) {
|
|||||||
$months = array_slice($months, 0, 7);
|
$months = array_slice($months, 0, 7);
|
||||||
sort($allCategories);
|
sort($allCategories);
|
||||||
|
|
||||||
// Définition de la classe couleur selon le propriétaire
|
$safeOwnerCls = htmlspecialchars(str_replace(' ', '_', $currentOwner), ENT_QUOTES);
|
||||||
$ownerTextClass = '';
|
$ownerColor = $personColors[$currentOwner] ?? '#1e293b';
|
||||||
if ($currentOwner === 'Alex') $ownerTextClass = 'txt-alex';
|
|
||||||
elseif ($currentOwner === 'Laia') $ownerTextClass = 'txt-laia';
|
|
||||||
else $ownerTextClass = 'txt-global'; // Pour Pol et Pep
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; margin-top: <?= ($requestedOwner === 'Nens' && $currentOwner !== 'Pol') ? '40px' : '0' ?>;">
|
<div class="epargne-header-bar <?= ($requestedOwner === 'KIDS' && $index > 0) ? 'pf-mt-40' : '' ?>">
|
||||||
<div style="flex-grow: 1;">
|
<div class="pf-flex-1">
|
||||||
<?php if ($requestedOwner === 'Nens'):
|
<?php if ($requestedOwner === 'KIDS'): ?>
|
||||||
$themeClass = 'theme-' . strtolower($currentOwner);
|
<h3 class="nens-title" style="--owner-color: <?= htmlspecialchars($ownerColor) ?>;">
|
||||||
?>
|
<?= htmlspecialchars($currentOwner) ?>
|
||||||
<h3 class="nens-title <?= $themeClass ?>" style="margin:0; font-size:1.2rem;">
|
|
||||||
<?= $currentOwner ?>
|
|
||||||
</h3>
|
</h3>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; gap: 10px; align-items: center;">
|
<div class="pf-flex-gap-12-center">
|
||||||
<?php if (!empty($months)): ?>
|
<?php if (!empty($months)): ?>
|
||||||
<button onclick="duplicateLastMonth('<?= $months[0] ?>', '<?= $currentOwner ?>')" class="pf-btn btn-secondary">
|
<button onclick="duplicateLastMonth('<?= $months[0] ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')" class="pf-btn btn-secondary">
|
||||||
🔁 <?= tr('bud_sav_add_one_month') ?>
|
🔁 <?= tr('bud_sav_add_one_month') ?>
|
||||||
</button>
|
</button>
|
||||||
|
<button onclick="promptNewSavingsLine('<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>', '<?= $months[0] ?>')" class="pf-btn">
|
||||||
|
+ <?= tr('bud_sav_new_line') ?>
|
||||||
|
</button>
|
||||||
|
<?php else: ?>
|
||||||
|
<button onclick="openCustomSavingsModal('<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')" class="pf-btn">
|
||||||
|
+ <?= tr('bud_sav_add_month') ?>
|
||||||
|
</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<button onclick="openCustomSavingsModal('<?= $currentOwner ?>')" class="pf-btn">
|
|
||||||
+ <?= tr('bud_sav_add_month') ?>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive" style="background:white; border-radius:16px; box-shadow:var(--shadow-sm); border:1px solid #e2e8f0;">
|
<div class="table-responsive pf-card-epargne">
|
||||||
<?php if (empty($months)): ?>
|
<?php if (empty($months)): ?>
|
||||||
<div style="padding: 30px; text-align: center; color: #64748b;">
|
<div class="pf-empty-dashed">
|
||||||
<p><?= sprintf(tr('bud_sav_no_data'), htmlspecialchars($currentOwner)) ?></p>
|
<p><?= sprintf(tr('bud_sav_no_data'), htmlspecialchars($currentOwner)) ?></p>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<table class="pf-table savings-table nens-table theme-<?= strtolower($currentOwner) ?>" style="margin-top:0; box-shadow:none; border-radius:16px;">
|
<div class="epargne-grid-table" style="--cols: <?= count($months) ?>; --owner-color: <?= htmlspecialchars($ownerColor) ?>;">
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="sticky-col" style="background:#f8fafc;"><?= tr('bud_sav_post_month') ?></th>
|
|
||||||
<?php foreach ($months as $month): ?>
|
|
||||||
<th>
|
|
||||||
<div class="month-header-container" style="display: flex; flex-direction: column; align-items: center; gap: 6px;">
|
|
||||||
<div style="display:flex; flex-direction:column; text-align:center;">
|
|
||||||
<span class="month-name" style="text-transform:capitalize;"><?= getMonthName($month) ?></span>
|
|
||||||
<?php
|
|
||||||
if (isset($cycleConfigs[$month]) && !empty($cycleConfigs[$month]['start_date'])) {
|
|
||||||
$cStart = date('d/m', strtotime($cycleConfigs[$month]['start_date']));
|
|
||||||
echo "<span style='font-size:0.75rem; font-weight:normal; color:#64748b;'>" . sprintf(tr('bud_sav_from_date'), $cStart) . "</span>";
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</div>
|
|
||||||
<div class="month-actions" style="justify-content: center; width: 100%;">
|
|
||||||
<button class="btn-icon-small btn-safe-click" title="<?= tr('bud_sav_edit_modal') ?>"
|
|
||||||
data-json="<?= htmlspecialchars(json_encode($data[$month] ?? []), ENT_QUOTES, 'UTF-8') ?>"
|
|
||||||
onclick='editCustomSavingsMonth("<?= $month ?>", "<?= $currentOwner ?>", JSON.parse(this.getAttribute("data-json")))'>
|
|
||||||
✏️
|
|
||||||
</button>
|
|
||||||
<button class="btn-icon-small btn-safe-click" title="<?= tr('bud_sav_delete_month') ?>"
|
|
||||||
onclick="deleteEntireMonth('<?= $month ?>', '<?= $currentOwner ?>')"
|
|
||||||
style="color: #ef4444; border-color: #fca5a5; background: #fef2f2;">
|
|
||||||
🗑️
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr class="row-total">
|
|
||||||
<td class="sticky-col"><strong><?= tr('bud_sav_total_bank') ?></strong></td>
|
|
||||||
<?php foreach ($months as $month):
|
|
||||||
$val = $data[$month]['TOTAL_BANQUE'] ?? 0;
|
|
||||||
?>
|
|
||||||
<td class="text-center" style="padding:4px;">
|
|
||||||
<div style="display:flex; align-items:center; justify-content:center; gap:2px;">
|
|
||||||
<input type="number" step="0.01"
|
|
||||||
class="prev-input total-input-<?= $currentOwner ?>-<?= $month ?>"
|
|
||||||
style="width: 70px; font-weight:bold; color:#2563eb;"
|
|
||||||
value="<?= $val != 0 ? round($val) : '' ?>"
|
|
||||||
placeholder="0"
|
|
||||||
onchange="updateEpargneCell('<?= $month ?>', 'TOTAL_BANQUE', '<?= $currentOwner ?>', this)">
|
|
||||||
<span style="color:#2563eb; font-weight:bold; font-size:0.9rem;">€</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<?php foreach ($allCategories as $cat): ?>
|
<div class="eg-header-row">
|
||||||
<tr>
|
<div class="eg-cell eg-sticky"><?= tr('bud_sav_post_month') ?></div>
|
||||||
<td class="sticky-col"><?= htmlspecialchars($cat) ?></td>
|
<?php foreach ($months as $month): ?>
|
||||||
<?php foreach ($months as $month):
|
<div class="eg-cell">
|
||||||
$amount = $data[$month][$cat] ?? 0;
|
<div class="month-header-container">
|
||||||
?>
|
<div class="month-header-title">
|
||||||
<td class="text-center" style="padding:4px;">
|
<span class="month-name text-inherit"><?= getMonthName($month) ?></span>
|
||||||
<div style="display:flex; align-items:center; justify-content:center; gap:2px;">
|
<?php
|
||||||
<input type="number" step="0.01"
|
if (isset($cycleConfigs[$month]) && !empty($cycleConfigs[$month]['start_date'])) {
|
||||||
class="prev-input <?= $ownerTextClass ?> cat-input-<?= $currentOwner ?>-<?= $month ?>"
|
$cStart = date('d/m', strtotime($cycleConfigs[$month]['start_date']));
|
||||||
style="width: 70px;"
|
echo "<span class='month-cycle-start'>" . sprintf(tr('bud_sav_from_date'), $cStart) . "</span>";
|
||||||
value="<?= $amount != 0 ? round($amount) : '' ?>"
|
}
|
||||||
placeholder="-"
|
?>
|
||||||
onchange="updateEpargneCell('<?= $month ?>', '<?= htmlspecialchars($cat, ENT_QUOTES) ?>', '<?= $currentOwner ?>', this)">
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
<div class="month-actions">
|
||||||
<?php endforeach; ?>
|
<button class="btn-icon-small btn-safe-click" title="<?= tr('bud_sav_edit_modal') ?>"
|
||||||
</tr>
|
data-json="<?= htmlspecialchars(json_encode($data[$month] ?? []), ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
onclick='editCustomSavingsMonth("<?= $month ?>", "<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>", JSON.parse(this.getAttribute("data-json")))'>
|
||||||
|
✏️
|
||||||
|
</button>
|
||||||
|
<button class="btn-icon-small btn-safe-click btn-danger-soft" title="<?= tr('bud_sav_delete_month') ?>"
|
||||||
|
onclick="deleteEntireMonth('<?= $month ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')">
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
<tr class="row-extres">
|
<div class="eg-row eg-total-row">
|
||||||
<td class="sticky-col"><strong><?= tr('bud_sav_extra') ?></strong></td>
|
<div class="eg-cell eg-sticky"><strong><?= tr('bud_sav_total_bank') ?></strong></div>
|
||||||
<?php foreach ($months as $month):
|
<?php foreach ($months as $month):
|
||||||
$total = $data[$month]['TOTAL_BANQUE'] ?? 0;
|
$val = $data[$month]['TOTAL_BANQUE'] ?? 0;
|
||||||
$sum = 0;
|
?>
|
||||||
foreach ($allCategories as $cat) $sum += ($data[$month][$cat] ?? 0);
|
<div class="eg-cell">
|
||||||
$extra = $total - $sum;
|
<div class="eg-cell-content">
|
||||||
?>
|
<input type="number" step="0.01"
|
||||||
<td class="text-center font-bold sum-target" id="extra_<?= $currentOwner ?>_<?= $month ?>" style="color: <?= $extra >= 0 ? '#10b981' : '#ef4444' ?>; padding:12px;">
|
class="prev-input eg-input eg-input-total total-input-<?= $safeOwnerCls ?>-<?= $month ?>"
|
||||||
<?= number_format($extra, 0, ',', ' ') ?> €
|
value="<?= $val != 0 ? round($val) : '' ?>"
|
||||||
</td>
|
placeholder="0"
|
||||||
<?php endforeach; ?>
|
onchange="updateEpargneCell('<?= $month ?>', 'TOTAL_BANQUE', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>', this)">
|
||||||
</tr>
|
<span class="eg-currency-symbol">€</span>
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php foreach ($allCategories as $cat): ?>
|
||||||
|
<div class="eg-row">
|
||||||
|
<div class="eg-cell eg-sticky"><?= htmlspecialchars($cat) ?></div>
|
||||||
|
<?php foreach ($months as $month):
|
||||||
|
$amount = $data[$month][$cat] ?? 0;
|
||||||
|
?>
|
||||||
|
<div class="eg-cell">
|
||||||
|
<input type="number" step="0.01"
|
||||||
|
class="prev-input eg-input cat-input-<?= $safeOwnerCls ?>-<?= $month ?>"
|
||||||
|
value="<?= $amount != 0 ? round($amount) : '' ?>"
|
||||||
|
placeholder="-"
|
||||||
|
onchange="updateEpargneCell('<?= $month ?>', '<?= htmlspecialchars($cat, ENT_QUOTES) ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>', this)">
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<div class="eg-row eg-extres-row">
|
||||||
|
<div class="eg-cell eg-sticky"><strong><?= tr('bud_sav_extra') ?></strong></div>
|
||||||
|
<?php foreach ($months as $month):
|
||||||
|
$total = $data[$month]['TOTAL_BANQUE'] ?? 0;
|
||||||
|
$sum = 0;
|
||||||
|
foreach ($allCategories as $cat) $sum += ($data[$month][$cat] ?? 0);
|
||||||
|
$extra = $total - $sum;
|
||||||
|
?>
|
||||||
|
<div class="eg-cell font-bold sum-target <?= $extra >= 0 ? 'text-success' : 'text-danger' ?>" id="extra_<?= $safeOwnerCls ?>_<?= $month ?>">
|
||||||
|
<?= number_format($extra, 0, ',', ' ') ?> €
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="savingsModal" class="pf-modal">
|
<div id="savingsModal" class="pf-modal">
|
||||||
<div class="pf-modal-content" style="max-width: 600px; width: 95%;">
|
<div class="pf-modal-content modal-epargne">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
<div class="pf-modal-header">
|
||||||
<h3 id="savingsModalTitle" class="pf-modal-title" style="margin:0;"><?= tr('bud_sav_modal_title_add') ?></h3>
|
<h3 id="savingsModalTitle" class="pf-modal-title"><?= tr('bud_sav_modal_title_add') ?></h3>
|
||||||
<button type="button" onclick="document.getElementById('savingsModal').style.display='none'; document.body.classList.remove('no-scroll');" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">×</button>
|
<button type="button" onclick="document.getElementById('savingsModal').classList.remove('open'); document.body.classList.remove('no-scroll');" class="pf-modal-close">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form action="modules/budget/includes/api/save-savings.php" method="POST" id="savingsForm">
|
<form action="/modules/budget/includes/api/save-savings.php" method="POST" id="savingsForm">
|
||||||
<input type="hidden" name="owner" id="sav_owner">
|
<input type="hidden" name="owner" id="sav_owner">
|
||||||
<input type="hidden" name="redirect_tab" id="redirect_tab" value="<?= htmlspecialchars($requestedOwner) ?>">
|
<input type="hidden" name="redirect_tab" id="redirect_tab" value="<?= htmlspecialchars($requestedOwner) ?>">
|
||||||
<input type="hidden" name="month_date" id="sav_date_hidden">
|
<input type="hidden" name="month_date" id="sav_date_hidden">
|
||||||
|
|
||||||
<div style="display:flex; gap:15px; margin-bottom:20px;">
|
<div class="pf-form-row">
|
||||||
<div class="form-group" style="flex:1; margin:0;">
|
<div class="pf-form-group pf-flex-1 pf-m-0">
|
||||||
<label class="pf-label"><?= tr('bud_sav_month_concerned') ?></label>
|
<label class="pf-label"><?= tr('bud_sav_month_concerned') ?></label>
|
||||||
<input type="month" id="sav_month" required class="pf-input">
|
<input type="month" id="sav_month" required class="pf-input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" style="flex:1; margin:0;">
|
<div class="pf-form-group pf-flex-1 pf-m-0">
|
||||||
<label class="pf-label"><?= tr('bud_sav_total_bank_eur') ?></label>
|
<label class="pf-label"><?= tr('bud_sav_total_bank_eur') ?></label>
|
||||||
<input type="number" step="0.01" name="values[TOTAL_BANQUE]" id="sav_total" required class="pf-input no-spinners" style="font-weight:bold; color:#2563eb;">
|
<input type="number" step="0.01" name="values[TOTAL_BANQUE]" id="sav_total" required class="pf-input no-spinners pf-input-total">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="separator" style="margin: 20px 0; border-bottom: 1px solid #e2e8f0;"></div>
|
<hr class="pf-divider">
|
||||||
|
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px;">
|
<div class="ventilation-header">
|
||||||
<div>
|
<div>
|
||||||
<h4 style="margin:0; font-size:1rem; color:#1e293b;"><?= tr('bud_sav_ventilation') ?></h4>
|
<h4 class="pf-m-0 text-main"><?= tr('bud_sav_ventilation') ?></h4>
|
||||||
<span style="font-size:0.8rem; color:#64748b;"><?= tr('bud_sav_adj_help') ?></span>
|
<span class="pf-muted-note"><?= tr('bud_sav_adj_help') ?></span>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="pf-btn btn-secondary" onclick="addCustomEpargneLine()" style="padding:4px 10px; height:auto; width:auto; font-size:0.9rem;">+ <?= tr('bud_sav_add_line') ?></button>
|
<button type="button" class="pf-btn btn-secondary pf-btn-sm" onclick="addCustomEpargneLine()">+ <?= tr('bud_sav_add_line') ?></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex; gap:10px; padding:0 5px 5px 5px; font-size:0.8rem; color:#64748b; font-weight:600;">
|
<div class="ventilation-cols">
|
||||||
<div style="flex:2;"><?= tr('bud_category') ?></div>
|
<div class="vl-col-name"><?= tr('bud_category') ?></div>
|
||||||
<div style="width:100px;"><?= tr('bud_sav_current') ?></div>
|
<div class="vl-col-base"><?= tr('bud_sav_current') ?></div>
|
||||||
<div style="width:90px;"><?= tr('bud_sav_adjust') ?></div>
|
<div class="vl-col-adj"><?= tr('bud_sav_adjust') ?></div>
|
||||||
<div style="width:100px;"><?= tr('bud_sav_new') ?></div>
|
<div class="vl-col-final"><?= tr('bud_sav_new') ?></div>
|
||||||
<div style="width:28px;"></div>
|
<div class="vl-col-btn"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="linesContainer" style="max-height: 350px; overflow-y: auto; padding-right:5px; display:flex; flex-direction:column; gap:10px;">
|
<div id="linesContainer" class="ventilation-list"></div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-top:25px; display:flex; justify-content:flex-end; gap:10px;">
|
<div class="modal-footer">
|
||||||
<button type="button" onclick="document.getElementById('savingsModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-btn btn-secondary" style="width:auto; margin:0;"><?= tr('btn_cancel') ?></button>
|
<button type="button" onclick="document.getElementById('savingsModal').classList.remove('open'); document.body.classList.remove('no-scroll');" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
|
||||||
<button type="submit" class="pf-btn" style="width:auto; margin:0;"><?= tr('btn_save') ?></button>
|
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -244,7 +264,6 @@ function getMonthName($dateString) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// --- 1. SÉCURISATION TRADUCTIONS ET LANGUE ---
|
|
||||||
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
||||||
window.I18N = {
|
window.I18N = {
|
||||||
...(window.I18N || {}),
|
...(window.I18N || {}),
|
||||||
@@ -253,17 +272,48 @@ window.I18N = {
|
|||||||
'bud_sav_ph_name': <?= json_encode(tr('bud_sav_ph_name')) ?>,
|
'bud_sav_ph_name': <?= json_encode(tr('bud_sav_ph_name')) ?>,
|
||||||
'bud_sav_confirm_delete_month': <?= json_encode(tr('bud_sav_confirm_delete_month')) ?>,
|
'bud_sav_confirm_delete_month': <?= json_encode(tr('bud_sav_confirm_delete_month')) ?>,
|
||||||
'bud_sav_prompt_duplicate': <?= json_encode(tr('bud_sav_prompt_duplicate')) ?>,
|
'bud_sav_prompt_duplicate': <?= json_encode(tr('bud_sav_prompt_duplicate')) ?>,
|
||||||
|
'bud_sav_prompt_new_line': <?= json_encode(tr('bud_sav_prompt_new_line')) ?>,
|
||||||
'bud_err_tech': <?= json_encode(tr('bud_err_tech')) ?>,
|
'bud_err_tech': <?= json_encode(tr('bud_err_tech')) ?>,
|
||||||
'bud_err_server': <?= json_encode(tr('bud_err_server')) ?>,
|
'bud_err_server': <?= json_encode(tr('bud_err_server')) ?>,
|
||||||
'bud_err_network_dup': <?= json_encode(tr('bud_err_network_dup')) ?>,
|
'bud_err_network_dup': <?= json_encode(tr('bud_err_network_dup')) ?>,
|
||||||
'bud_sav_saving': <?= json_encode(tr('bud_sav_saving')) ?>,
|
'bud_sav_saving': <?= json_encode(tr('bud_sav_saving')) ?>,
|
||||||
'bud_err_delete': <?= json_encode(tr('bud_err_delete')) ?>
|
'bud_err_delete': <?= json_encode(tr('bud_err_delete')) ?>,
|
||||||
|
'btn_delete': <?= json_encode(tr('btn_delete')) ?>
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 2. GESTION DE L'ÉDITION INVISIBLE EN DIRECT ---
|
const systemCurrency = (typeof window.CONFIG !== 'undefined' && window.CONFIG.CURRENCY) ? window.CONFIG.CURRENCY : '€';
|
||||||
const cycleConfigs = <?= json_encode($cycleConfigs ?? []) ?>;
|
const cycleConfigs = <?= json_encode($cycleConfigs ?? []) ?>;
|
||||||
|
|
||||||
function updateEpargneCell(month, category, owner, inputEl) {
|
async function promptNewSavingsLine(owner, latestMonth) {
|
||||||
|
const catName = prompt(window.I18N['bud_sav_prompt_new_line']);
|
||||||
|
if (!catName || catName.trim() === "") return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', 'update_single_entry');
|
||||||
|
formData.append('month_date', latestMonth);
|
||||||
|
formData.append('category', catName.trim());
|
||||||
|
formData.append('owner', owner);
|
||||||
|
formData.append('amount', 0);
|
||||||
|
formData.append('force_insert', '1'); // 🛡️ Empêche la suppression auto par l'API
|
||||||
|
formData.append('ajax', '1');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await pachaFetch('/modules/budget/includes/api/save-savings.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
showToast(window.I18N['bud_err_tech'] + " : " + (result.error || "Erreur"), 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showToast(window.I18N['bud_err_tech'], 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateEpargneCell(month, category, owner, inputEl) {
|
||||||
const val = parseFloat(inputEl.value) || 0;
|
const val = parseFloat(inputEl.value) || 0;
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('action', 'update_single_entry');
|
formData.append('action', 'update_single_entry');
|
||||||
@@ -271,26 +321,40 @@ function updateEpargneCell(month, category, owner, inputEl) {
|
|||||||
formData.append('category', category);
|
formData.append('category', category);
|
||||||
formData.append('owner', owner);
|
formData.append('owner', owner);
|
||||||
formData.append('amount', val);
|
formData.append('amount', val);
|
||||||
|
formData.append('ajax', '1');
|
||||||
|
|
||||||
fetch('modules/budget/includes/api/save-savings.php', {
|
try {
|
||||||
method: 'POST',
|
const result = await pachaFetch('/modules/budget/includes/api/save-savings.php', { method: 'POST', body: formData });
|
||||||
body: formData
|
if (!result.success) {
|
||||||
}).catch(err => alert(window.I18N['bud_err_tech'] || 'Erreur technique'));
|
alert(window.I18N['bud_err_tech'] + " : " + (result.error || "Erreur serveur"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showToast(window.I18N['bud_err_tech'], 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const totalInput = document.querySelector(`.total-input-${owner}-${month}`);
|
const safeOwnerClass = owner.replace(/\s+/g, '_');
|
||||||
|
const totalInput = document.querySelector(`.total-input-${CSS.escape(safeOwnerClass)}-${month}`);
|
||||||
const totalVal = parseFloat(totalInput ? totalInput.value : 0) || 0;
|
const totalVal = parseFloat(totalInput ? totalInput.value : 0) || 0;
|
||||||
|
|
||||||
let sumCats = 0;
|
let sumCats = 0;
|
||||||
document.querySelectorAll(`.cat-input-${owner}-${month}`).forEach(inp => {
|
document.querySelectorAll(`.cat-input-${CSS.escape(safeOwnerClass)}-${month}`).forEach(inp => {
|
||||||
sumCats += parseFloat(inp.value) || 0;
|
sumCats += parseFloat(inp.value) || 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
const extra = totalVal - sumCats;
|
const extra = totalVal - sumCats;
|
||||||
const extraCell = document.getElementById(`extra_${owner}_${month}`);
|
const extraCell = document.getElementById(`extra_${safeOwnerClass}_${month}`);
|
||||||
|
|
||||||
if (extraCell) {
|
if (extraCell) {
|
||||||
extraCell.innerText = Math.round(extra).toLocaleString(window.appLang) + ' €';
|
extraCell.innerText = Math.round(extra).toLocaleString(window.appLang) + ' €';
|
||||||
extraCell.style.color = extra >= 0 ? '#10b981' : '#ef4444';
|
if (extra >= 0) {
|
||||||
|
extraCell.classList.remove('text-danger');
|
||||||
|
extraCell.classList.add('text-success');
|
||||||
|
} else {
|
||||||
|
extraCell.classList.remove('text-success');
|
||||||
|
extraCell.classList.add('text-danger');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isSumModeActive) updateSumResult();
|
if(isSumModeActive) updateSumResult();
|
||||||
@@ -302,20 +366,20 @@ function addCustomEpargneLine(catName = '', amount = '') {
|
|||||||
const inputName = catName ? `values[${catName}]` : '';
|
const inputName = catName ? `values[${catName}]` : '';
|
||||||
|
|
||||||
const html = `
|
const html = `
|
||||||
<div class="ventilation-line" style="display:flex; gap:10px; align-items:center; background:#f8fafc; padding:8px; border-radius:8px; border:1px solid #e2e8f0;">
|
<div class="ventilation-line">
|
||||||
<div style="flex:2;">
|
<div class="vl-col-name">
|
||||||
<input type="text" class="pf-input cat-name-input" value="${catName}" placeholder="${window.I18N['bud_sav_ph_name'] || 'Catégorie'}" oninput="updateCustomFieldName(this)" style="padding:6px; font-size:0.9rem;" required>
|
<input type="text" class="pf-input cat-name-input pf-input-sm" value="${catName}" placeholder="${window.I18N['bud_sav_ph_name'] || 'Catégorie'}" oninput="updateCustomFieldName(this)" required>
|
||||||
</div>
|
</div>
|
||||||
<div style="width:100px;">
|
<div class="vl-col-base">
|
||||||
<input type="number" step="0.01" class="pf-input base-amount no-spinners" value="${baseAmount}" oninput="recalculateCustomLine(this)" style="padding:6px; font-size:0.9rem; background:#fff;">
|
<input type="number" step="0.01" class="pf-input base-amount no-spinners pf-input-sm bg-white" value="${baseAmount}" oninput="recalculateCustomLine(this)">
|
||||||
</div>
|
</div>
|
||||||
<div style="width:90px;">
|
<div class="vl-col-adj">
|
||||||
<input type="number" step="0.01" class="pf-input adjustment-amount no-spinners" placeholder="+ / -" oninput="recalculateCustomLine(this)" style="padding:6px; font-size:0.9rem; color:#f59e0b; font-weight:bold;">
|
<input type="number" step="0.01" class="pf-input adjustment-amount no-spinners pf-input-sm vl-input-adj" placeholder="+ / -" oninput="recalculateCustomLine(this)">
|
||||||
</div>
|
</div>
|
||||||
<div style="width:100px;">
|
<div class="vl-col-final">
|
||||||
<input type="number" step="0.01" name="${inputName}" class="pf-input final-amount no-spinners" value="${baseAmount}" style="padding:6px; font-size:0.9rem; font-weight:bold; background:#e0f2fe; border-color:#bae6fd; color:#0369a1;" readonly>
|
<input type="number" step="0.01" name="${inputName}" class="pf-input final-amount no-spinners pf-input-sm vl-input-final" value="${baseAmount}" readonly>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" onclick="this.parentElement.remove()" style="width:28px; height:28px; border:none; background:#fee2e2; color:#ef4444; border-radius:4px; cursor:pointer; display:flex; align-items:center; justify-content:center; font-weight:bold;">×</button>
|
<button type="button" class="btn-remove-line" onclick="this.parentElement.remove()" title="${window.I18N['btn_delete']}">×</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
container.insertAdjacentHTML('beforeend', html);
|
container.insertAdjacentHTML('beforeend', html);
|
||||||
@@ -361,7 +425,7 @@ function editCustomSavingsMonth(monthDate, owner, rowData) {
|
|||||||
|
|
||||||
if (container.children.length === 0) addCustomEpargneLine();
|
if (container.children.length === 0) addCustomEpargneLine();
|
||||||
|
|
||||||
document.getElementById('savingsModal').style.display = 'flex';
|
document.getElementById('savingsModal').classList.add('open');
|
||||||
document.body.classList.add('no-scroll');
|
document.body.classList.add('no-scroll');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,21 +440,13 @@ function openCustomSavingsModal(owner) {
|
|||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
addCustomEpargneLine();
|
addCustomEpargneLine();
|
||||||
|
|
||||||
document.getElementById('savingsModal').style.display = 'flex';
|
document.getElementById('savingsModal').classList.add('open');
|
||||||
document.body.classList.add('no-scroll');
|
document.body.classList.add('no-scroll');
|
||||||
}
|
}
|
||||||
|
|
||||||
window.onclick = function(event) {
|
|
||||||
const modal = document.getElementById('savingsModal');
|
|
||||||
if (event.target == modal) {
|
|
||||||
modal.style.display = 'none';
|
|
||||||
document.body.classList.remove('no-scroll');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const savingsForm = document.getElementById('savingsForm');
|
const savingsForm = document.getElementById('savingsForm');
|
||||||
if (savingsForm) {
|
if (savingsForm) {
|
||||||
savingsForm.addEventListener('submit', function(e) {
|
savingsForm.addEventListener('submit', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const ym = document.getElementById('sav_month').value;
|
const ym = document.getElementById('sav_month').value;
|
||||||
@@ -404,24 +460,27 @@ if (savingsForm) {
|
|||||||
submitBtn.disabled = true;
|
submitBtn.disabled = true;
|
||||||
|
|
||||||
const formData = new FormData(this);
|
const formData = new FormData(this);
|
||||||
|
formData.append('ajax', '1');
|
||||||
|
|
||||||
const actionUrl = this.getAttribute('action');
|
try {
|
||||||
const finalUrl = actionUrl.startsWith('/') ? actionUrl.substring(1) : actionUrl;
|
const result = await pachaFetch('/modules/budget/includes/api/save-savings.php', { method: 'POST', body: formData });
|
||||||
|
if (result.success) {
|
||||||
fetch(finalUrl, { method: 'POST', body: formData })
|
window.location.reload();
|
||||||
.then(response => response.text())
|
} else {
|
||||||
.then(text => { window.location.reload(); })
|
alert((window.I18N['bud_err_server'] || 'Erreur serveur : ') + (result.error || "Inconnue"));
|
||||||
.catch(error => {
|
submitBtn.innerText = originalText;
|
||||||
console.error("Erreur:", error);
|
submitBtn.disabled = false;
|
||||||
alert(window.I18N['bud_err_tech'] || 'Erreur Technique');
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showToast(window.I18N['bud_err_tech'], 'error');
|
||||||
submitBtn.innerText = originalText;
|
submitBtn.innerText = originalText;
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteEntireMonth(monthDate, owner) {
|
async function deleteEntireMonth(monthDate, owner) {
|
||||||
const rawMsg = window.I18N['bud_sav_confirm_delete_month'] || "Supprimer %m pour %o ?";
|
const rawMsg = window.I18N['bud_sav_confirm_delete_month'];
|
||||||
const msg = rawMsg.replace('%m', monthDate).replace('%o', owner);
|
const msg = rawMsg.replace('%m', monthDate).replace('%o', owner);
|
||||||
if (!confirm(msg)) return;
|
if (!confirm(msg)) return;
|
||||||
|
|
||||||
@@ -429,13 +488,18 @@ function deleteEntireMonth(monthDate, owner) {
|
|||||||
formData.append("action", "delete_month_global");
|
formData.append("action", "delete_month_global");
|
||||||
formData.append("month_date", monthDate);
|
formData.append("month_date", monthDate);
|
||||||
formData.append("owner", owner);
|
formData.append("owner", owner);
|
||||||
|
formData.append("ajax", "1");
|
||||||
|
|
||||||
fetch("modules/budget/includes/api/save-savings.php", { method: "POST", body: formData })
|
try {
|
||||||
.then(() => window.location.reload())
|
const result = await pachaFetch('/modules/budget/includes/api/save-savings.php', { method: "POST", body: formData });
|
||||||
.catch(err => alert(window.I18N['bud_err_delete'] || 'Erreur lors de la suppression'));
|
if (result.success) window.location.reload();
|
||||||
|
else alert(result.error || window.I18N['bud_err_delete']);
|
||||||
|
} catch(err) {
|
||||||
|
showToast(window.I18N['bud_err_tech'], 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function duplicateLastMonth(lastMonthDate, owner) {
|
async function duplicateLastMonth(lastMonthDate, owner) {
|
||||||
let dateObj = new Date(lastMonthDate);
|
let dateObj = new Date(lastMonthDate);
|
||||||
dateObj.setMonth(dateObj.getMonth() + 1);
|
dateObj.setMonth(dateObj.getMonth() + 1);
|
||||||
let year = dateObj.getFullYear();
|
let year = dateObj.getFullYear();
|
||||||
@@ -447,16 +511,13 @@ function duplicateLastMonth(lastMonthDate, owner) {
|
|||||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sourceName = formatMonth(lastMonthDate);
|
|
||||||
const targetName = formatMonth(nextMonthStr);
|
|
||||||
|
|
||||||
let defaultTotal = "";
|
let defaultTotal = "";
|
||||||
if (cycleConfigs[nextMonthStr] && cycleConfigs[nextMonthStr].start_balance !== undefined) {
|
if (cycleConfigs[nextMonthStr] && cycleConfigs[nextMonthStr].start_balance !== undefined) {
|
||||||
defaultTotal = cycleConfigs[nextMonthStr].start_balance;
|
defaultTotal = cycleConfigs[nextMonthStr].start_balance;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawMsg = window.I18N['bud_sav_prompt_duplicate'] || "Dupliquer %s vers %t1 ?";
|
const rawMsg = window.I18N['bud_sav_prompt_duplicate'];
|
||||||
const message = rawMsg.replace('%s', sourceName).replace('%t1', targetName).replace('%t2', targetName);
|
const message = rawMsg.replace('%s', formatMonth(lastMonthDate)).replace(/%t[12]/g, formatMonth(nextMonthStr));
|
||||||
|
|
||||||
let newTotal = prompt(message, defaultTotal);
|
let newTotal = prompt(message, defaultTotal);
|
||||||
|
|
||||||
@@ -467,22 +528,18 @@ function duplicateLastMonth(lastMonthDate, owner) {
|
|||||||
formData.append("target_date", nextMonthStr);
|
formData.append("target_date", nextMonthStr);
|
||||||
formData.append("new_total", newTotal);
|
formData.append("new_total", newTotal);
|
||||||
formData.append("owner", owner);
|
formData.append("owner", owner);
|
||||||
|
formData.append("ajax", "1");
|
||||||
|
|
||||||
fetch("modules/budget/includes/api/save-savings.php", { method: "POST", body: formData })
|
try {
|
||||||
.then(async r => {
|
const result = await pachaFetch('/modules/budget/includes/api/save-savings.php', { method: "POST", body: formData });
|
||||||
const text = await r.text();
|
if (result.success) {
|
||||||
try {
|
|
||||||
const d = JSON.parse(text);
|
|
||||||
if (d.success) window.location.reload();
|
|
||||||
else alert((window.I18N['bud_err_server'] || 'Erreur serveur : ') + (d.error || "Inconnue"));
|
|
||||||
} catch(e) {
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert((window.I18N['bud_err_server'] || 'Erreur serveur : ') + (result.error || "Inconnue"));
|
||||||
}
|
}
|
||||||
})
|
} catch(err) {
|
||||||
.catch(err => {
|
alert(window.I18N['bud_err_network_dup']);
|
||||||
console.error(err);
|
}
|
||||||
alert(window.I18N['bud_err_network_dup'] || 'Erreur réseau.');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,7 +585,7 @@ function updateSumResult() {
|
|||||||
total += val;
|
total += val;
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('sumResultValue').innerText = Math.round(total).toLocaleString(window.appLang) + ' ' + window.CONFIG.CURRENCY;
|
document.getElementById('sumResultValue').innerText = Math.round(total).toLocaleString(window.appLang) + ' ' + systemCurrency;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener('click', function(e) {
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
<?php
|
||||||
|
// modules/budget/views/provisions.php
|
||||||
|
|
||||||
|
// --- RÉCUPÉRATION DES DÉPENSES PRÉVUES (MULTI-TENANT PDO) ---
|
||||||
|
$stmt = $pdo->query("SELECT id, title, amount, expected_date FROM pf_expected_expenses WHERE is_paid = 0 ORDER BY expected_date ASC");
|
||||||
|
$provisions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// --- RÉCUPÉRATION DES APPORTS ET DETTES (CLEARING) ---
|
||||||
|
$currentYear = date('Y');
|
||||||
|
|
||||||
|
// 1. Apports de base (Eco Family) des parents configurés pour l'année
|
||||||
|
$stmtConfig = $pdo->prepare("SELECT person, eco_family FROM pf_salary_config WHERE year = ?");
|
||||||
|
$stmtConfig->execute([$currentYear]);
|
||||||
|
$configs = $stmtConfig->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
|
|
||||||
|
// 2. Dettes non résolues par personne (issues des Avances & Tricount)
|
||||||
|
$stmtDebts = $pdo->query("SELECT payer, SUM(amount) as total_debt FROM pf_advances WHERE is_resolved = 0 GROUP BY payer");
|
||||||
|
$debts = $stmtDebts->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
|
|
||||||
|
function formatDate($dateString) {
|
||||||
|
if (!$dateString) return '';
|
||||||
|
return date('d/m/Y', strtotime($dateString));
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="budget-view">
|
||||||
|
<div class="view-header">
|
||||||
|
<h2><?= tr('budget_provisions_title') ?></h2>
|
||||||
|
|
||||||
|
<button class="pf-btn pf-btn-primary" onclick="openOptimizeModal()">
|
||||||
|
✨ <?= tr('btn_optimize_cashflow') ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pf-card">
|
||||||
|
<h3 class="pf-card-h2"><?= tr('add_new_provision') ?></h3>
|
||||||
|
|
||||||
|
<form id="form-add-provision" data-action="/modules/budget/includes/api/add-provision.php">
|
||||||
|
<div class="provisions-form-grid">
|
||||||
|
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label" for="prov-title"><?= tr('provision_label') ?></label>
|
||||||
|
<input type="text" id="prov-title" name="title" class="pf-input" placeholder="<?= tr('provision_placeholder_wood') ?>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label" for="prov-amount"><?= tr('amount') ?> (€)</label>
|
||||||
|
<input type="number" id="prov-amount" name="amount" class="pf-input no-spinners input-amount-highlight" step="0.01" min="0.01" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label" for="prov-date"><?= tr('expected_date') ?></label>
|
||||||
|
<input type="date" id="prov-date" name="expected_date" class="pf-input" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="pf-btn pf-btn-secondary">
|
||||||
|
<?= tr('btn_add') ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="budget-table-card table-responsive">
|
||||||
|
<?php if (empty($provisions)): ?>
|
||||||
|
<div class="optimization-hint" style="padding: 30px;">
|
||||||
|
<p><?= tr('no_provisions') ?></p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<table class="pf-table savings-table provisions-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?= tr('provision_label') ?></th>
|
||||||
|
<th style="width: 150px;"><?= tr('amount') ?></th>
|
||||||
|
<th style="width: 150px;"><?= tr('expected_date') ?></th>
|
||||||
|
<th style="width: 100px;"><?= tr('actions') ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($provisions as $p): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="prov-title-cell">
|
||||||
|
<?= htmlspecialchars($p['title']) ?>
|
||||||
|
</td>
|
||||||
|
<td class="prov-amount-cell">
|
||||||
|
<?= number_format($p['amount'], 2, ',', ' ') ?> €
|
||||||
|
</td>
|
||||||
|
<td class="prov-date-cell">
|
||||||
|
<?= formatDate($p['expected_date']) ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-right" style="padding-right: 15px;">
|
||||||
|
<button class="btn-icon-action delete btn-safe-click" title="<?= tr('btn_delete') ?>" onclick="deleteProvision(<?= $p['id'] ?>)">
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="optimizeModal" class="pf-modal">
|
||||||
|
<div class="pf-modal-content provisions-modal-content">
|
||||||
|
|
||||||
|
<div class="pf-modal-header">
|
||||||
|
<h3 class="pf-modal-title">🧠 <?= tr('optimization_assistant_title') ?></h3>
|
||||||
|
<button type="button" class="pf-modal-close" onclick="closeOptimizeModal()">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="form-optimize" data-action="/modules/budget/includes/api/optimize-cashflow.php">
|
||||||
|
<div id="dynamic-savings-inputs">
|
||||||
|
<?php if (empty($configs)): ?>
|
||||||
|
<p class="text-danger font-bold pf-muted-tiny"><?= tr('budget_opti_no_config') ?></p>
|
||||||
|
<input type="number" step="0.01" min="0" name="savings[Global]" class="pf-input no-spinners" required>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($configs as $person => $ecoBase):
|
||||||
|
$debt = $debts[$person] ?? 0;
|
||||||
|
?>
|
||||||
|
<div class="pf-form-group provision-person-card">
|
||||||
|
<label class="pf-label provision-person-header">
|
||||||
|
<strong class="text-main"><?= htmlspecialchars($person) ?></strong>
|
||||||
|
<span class="pf-muted-note">
|
||||||
|
<?php if ($debt > 0): ?>
|
||||||
|
<span class="text-danger font-bold">(Dette en attente : <?= (float)$debt ?> €)</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div class="provision-input-group">
|
||||||
|
<input type="number" step="0.01" min="0" name="savings[<?= htmlspecialchars($person) ?>]" class="pf-input no-spinners input-amount-success" value="<?= (float)$ecoBase ?>" required>
|
||||||
|
<span class="currency-symbol">€</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="pf-btn pf-btn-primary btn-block">
|
||||||
|
<?= tr('calculate') ?>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr class="pf-divider">
|
||||||
|
|
||||||
|
<div id="optimization-results">
|
||||||
|
<p class="optimization-hint">
|
||||||
|
Vérifiez vos apports théoriques ci-dessus pour calculer la stratégie de répartition (Clearing automatique si aucune dépense).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
||||||
|
window.I18N = {
|
||||||
|
...(window.I18N || {}),
|
||||||
|
'btn_delete': <?= json_encode(tr('btn_delete')) ?>,
|
||||||
|
'error_generic': <?= json_encode(tr('error_generic')) ?>,
|
||||||
|
'bud_err_tech': <?= json_encode(tr('bud_err_tech')) ?>,
|
||||||
|
'bud_err_server': <?= json_encode(tr('bud_err_server')) ?>
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 2. GESTION DU FORMULAIRE D'AJOUT ---
|
||||||
|
const formAdd = document.getElementById('form-add-provision');
|
||||||
|
if (formAdd) {
|
||||||
|
formAdd.addEventListener('submit', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const submitBtn = this.querySelector('button[type="submit"]');
|
||||||
|
const originalText = submitBtn.innerText;
|
||||||
|
submitBtn.innerText = '⏳';
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await pachaFetch(this.getAttribute('data-action'), {
|
||||||
|
method: 'POST',
|
||||||
|
body: new FormData(this)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert(result.message || window.I18N['error_generic']);
|
||||||
|
submitBtn.innerText = originalText;
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showToast(window.I18N['bud_err_tech'], 'error');
|
||||||
|
submitBtn.innerText = originalText;
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 3. GESTION DE LA SUPPRESSION ---
|
||||||
|
async function deleteProvision(id) {
|
||||||
|
if (!confirm(window.I18N['btn_delete'] + ' ?')) return;
|
||||||
|
try {
|
||||||
|
const result = await pachaFetch('/modules/budget/includes/api/delete-provision.php', {
|
||||||
|
method: "POST",
|
||||||
|
body: new URLSearchParams({ id: id })
|
||||||
|
});
|
||||||
|
if (result.success) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert(result.message);
|
||||||
|
}
|
||||||
|
} catch(err) {
|
||||||
|
showToast(window.I18N['bud_err_tech'], 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 4. CONTROL DE LA MODALE D'OPTIMISATION ---
|
||||||
|
function openOptimizeModal() {
|
||||||
|
document.getElementById('optimization-results').innerHTML = `
|
||||||
|
<p class="optimization-hint">
|
||||||
|
Vérifiez vos apports théoriques ci-dessus pour calculer la stratégie de répartition (Clearing automatique si aucune dépense).
|
||||||
|
</p>`;
|
||||||
|
document.getElementById('optimizeModal').classList.add('open');
|
||||||
|
document.body.classList.add('no-scroll');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeOptimizeModal() {
|
||||||
|
document.getElementById('optimizeModal').classList.remove('open');
|
||||||
|
document.body.classList.remove('no-scroll');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onclick = function(event) {
|
||||||
|
const modal = document.getElementById('optimizeModal');
|
||||||
|
if (event.target == modal) {
|
||||||
|
closeOptimizeModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 5. EXECUTION ET SUBMIT DE L'ALGORITHME (formOptimize) ---
|
||||||
|
const formOptimize = document.getElementById('form-optimize');
|
||||||
|
if (formOptimize) {
|
||||||
|
formOptimize.addEventListener('submit', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const resultsContainer = document.getElementById('optimization-results');
|
||||||
|
const submitBtn = this.querySelector('button[type="submit"]');
|
||||||
|
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
resultsContainer.innerHTML = '<div class="optimization-hint">⏳ Analyse des quinzaines bancaires...</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await pachaFetch(this.getAttribute('data-action'), {
|
||||||
|
method: 'POST',
|
||||||
|
body: new FormData(this)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
resultsContainer.innerHTML = result.html;
|
||||||
|
} else {
|
||||||
|
resultsContainer.innerHTML = `<p class="text-danger font-bold text-center">${result.message}</p>`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur d'optimisation :", error);
|
||||||
|
resultsContainer.innerHTML = `<p class="text-danger font-bold text-center">${window.I18N['bud_err_tech']}</p>`;
|
||||||
|
} finally {
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
+248
-265
@@ -1,46 +1,60 @@
|
|||||||
<?php
|
<?php
|
||||||
// modules/budget/views/recap.php
|
// modules/budget/views/recap.php
|
||||||
|
|
||||||
// 1. Récupération des Items du Budget
|
// 1. Récupération des Catégories dynamiques
|
||||||
$stmt = $pdo->query("SELECT * FROM pf_budget_items ORDER BY category DESC, sort_order ASC, name ASC");
|
$stmtCats = $pdo->query("SELECT code, label, icon, type FROM pf_budget_categories ORDER BY type DESC, label ASC");
|
||||||
$items = $stmt->fetchAll();
|
$dbCategories = $stmtCats->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
// 2. Déterminer quel est le mois de gestion "ouvert" par défaut
|
// 2. Récupération des Salaires/Mensualités configurés (Revenus automatiques)
|
||||||
|
$currentYear = isset($_GET['y']) ? (int)$_GET['y'] : date('Y');
|
||||||
|
$stmtSalaries = $pdo->prepare("SELECT id, person, mensualite FROM pf_salary_config WHERE year = ?");
|
||||||
|
$stmtSalaries->execute([$currentYear]);
|
||||||
|
$salaries = $stmtSalaries->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// 3. Récupération des Items du Budget (Charges Fixes et Estimations)
|
||||||
|
$stmt = $pdo->query("SELECT * FROM pf_budget_items WHERE category != 'SAVINGS' ORDER BY is_estimate ASC, sort_order ASC, name ASC");
|
||||||
|
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// 4. Gestion du mois actif
|
||||||
$stmtActive = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'active_gestion_month' LIMIT 1");
|
$stmtActive = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'active_gestion_month' LIMIT 1");
|
||||||
$defaultActiveMonth = $stmtActive->fetchColumn();
|
$defaultActiveMonth = $stmtActive->fetchColumn() ?: date('Y-m-01');
|
||||||
if (!$defaultActiveMonth) {
|
|
||||||
$defaultActiveMonth = date('Y-m-01');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Assigner le mois et l'année en fonction du mois ouvert
|
|
||||||
$currentMonth = isset($_GET['m']) ? str_pad((int)$_GET['m'], 2, '0', STR_PAD_LEFT) : date('m', strtotime($defaultActiveMonth));
|
$currentMonth = isset($_GET['m']) ? str_pad((int)$_GET['m'], 2, '0', STR_PAD_LEFT) : date('m', strtotime($defaultActiveMonth));
|
||||||
$currentYear = isset($_GET['y']) ? (int)$_GET['y'] : date('Y', strtotime($defaultActiveMonth));
|
|
||||||
$viewMonthDate = "$currentYear-$currentMonth-01";
|
$viewMonthDate = "$currentYear-$currentMonth-01";
|
||||||
|
|
||||||
$sqlReal = "SELECT budget_item_id, SUM(amount) as total_real
|
// 5. Récupération optimisée et unifiée du Réel
|
||||||
FROM pf_expenses
|
// On récupère tout en une seule requête pour éviter les décalages entre ID et Catégorie
|
||||||
WHERE gestion_month = ? AND budget_item_id IS NOT NULL
|
$stmtReal = $pdo->prepare("
|
||||||
GROUP BY budget_item_id";
|
SELECT budget_item_id, category, SUM(amount) as total_real
|
||||||
$stmtReal = $pdo->prepare($sqlReal);
|
FROM pf_expenses
|
||||||
|
WHERE gestion_month = ?
|
||||||
|
GROUP BY budget_item_id, category
|
||||||
|
");
|
||||||
$stmtReal->execute([$viewMonthDate]);
|
$stmtReal->execute([$viewMonthDate]);
|
||||||
$realTotals = $stmtReal->fetchAll(PDO::FETCH_KEY_PAIR); // Retourne un tableau [id => total]
|
$allExpenses = $stmtReal->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$sqlCatReal = "SELECT category, SUM(amount) as total_real
|
// On initialise les tableaux de correspondance
|
||||||
FROM pf_expenses
|
$realTotalsById = []; // Pour les lignes liées par ID
|
||||||
WHERE gestion_month = ? AND budget_item_id IS NULL
|
$realTotalsByCat = []; // Pour les lignes orphelines (NULL) liées par Catégorie
|
||||||
GROUP BY category";
|
|
||||||
$stmtCatReal = $pdo->prepare($sqlCatReal);
|
|
||||||
$stmtCatReal->execute([$viewMonthDate]);
|
|
||||||
$catTotals = $stmtCatReal->fetchAll(PDO::FETCH_KEY_PAIR);
|
|
||||||
|
|
||||||
$stmtLabels = $pdo->prepare("SELECT label, amount FROM pf_expenses WHERE gestion_month = ? AND budget_item_id IS NULL");
|
foreach ($allExpenses as $row) {
|
||||||
|
if (!empty($row['budget_item_id'])) {
|
||||||
|
$realTotalsById[$row['budget_item_id']] = (float)$row['total_real'];
|
||||||
|
} else {
|
||||||
|
$realTotalsByCat[$row['category']] = (float)$row['total_real'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmtSalariesReal = $pdo->prepare("SELECT salary_id, SUM(amount) as total_salary FROM pf_expenses WHERE gestion_month = ? AND salary_id IS NOT NULL GROUP BY salary_id");
|
||||||
|
$stmtSalariesReal->execute([$viewMonthDate]);
|
||||||
|
$realTotalsBySalary = $stmtSalariesReal->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
|
|
||||||
|
// Récupération des détails pour le mapping par mots-clés (si besoin de précision)
|
||||||
|
$stmtLabels = $pdo->prepare("SELECT label, amount, category FROM pf_expenses WHERE gestion_month = ? AND budget_item_id IS NULL");
|
||||||
$stmtLabels->execute([$viewMonthDate]);
|
$stmtLabels->execute([$viewMonthDate]);
|
||||||
$unlinkedExpenses = $stmtLabels->fetchAll(PDO::FETCH_ASSOC);
|
$unlinkedExpenses = $stmtLabels->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$moisFr = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
|
// Gestion des libellés et totaux
|
||||||
$monthTranslationKey = 'month_' . str_pad((int)$currentMonth, 2, '0', STR_PAD_LEFT);
|
$currentMonthName = tr('month_' . str_pad((int)$currentMonth, 2, '0', STR_PAD_LEFT)) . ' ' . $currentYear;
|
||||||
$currentMonthName = tr($monthTranslationKey) . ' ' . $currentYear;
|
|
||||||
|
|
||||||
$totalDepenses = 0;
|
$totalDepenses = 0;
|
||||||
$totalRevenus = 0;
|
$totalRevenus = 0;
|
||||||
?>
|
?>
|
||||||
@@ -64,124 +78,169 @@ $totalRevenus = 0;
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($items as $item):
|
<?php foreach ($salaries as $salary):
|
||||||
// --- 1. CALCUL DES TOTAUX PRÉVUS ---
|
$mensualite = (float)$salary['mensualite'];
|
||||||
$targetAbs = abs((float)$item['amount']);
|
$totalRevenus += $mensualite;
|
||||||
$amountToAdd = ($item['type'] === 'Annuel') ? $targetAbs / 12 : $targetAbs;
|
|
||||||
|
|
||||||
if ($item['category'] === 'income') $totalRevenus += $amountToAdd;
|
// Calcul du réel perçu
|
||||||
else $totalDepenses += $amountToAdd;
|
$realPerçu = isset($realTotalsBySalary[$salary['id']]) ? (float)$realTotalsBySalary[$salary['id']] : 0;
|
||||||
|
$gap = $realPerçu - $mensualite;
|
||||||
// --- 2. CALCUL DU RÉEL (Logique optimisée) ---
|
$isAutoChecked = ($realPerçu >= ($mensualite - 0.10));
|
||||||
$realSum = 0;
|
|
||||||
$hasMatchingExpense = false;
|
|
||||||
|
|
||||||
// A. Correspondance directe par ID
|
|
||||||
if (isset($realTotals[$item['id']])) {
|
|
||||||
$realSum = $realTotals[$item['id']];
|
|
||||||
$hasMatchingExpense = true;
|
|
||||||
}
|
|
||||||
// B. Correspondance par catégorie système (École, Essence, FMCG)
|
|
||||||
else {
|
|
||||||
$catKey = null;
|
|
||||||
if (trim($item['name']) === 'Estimacio escola') $catKey = 'School';
|
|
||||||
elseif (trim($item['name']) === 'Estimation gasolina') $catKey = 'Essence';
|
|
||||||
elseif (trim($item['name']) === 'Estimacio F&B & beauty') $catKey = 'FMCG';
|
|
||||||
|
|
||||||
if ($catKey && isset($catTotals[$catKey])) {
|
|
||||||
$realSum = $catTotals[$catKey];
|
|
||||||
$hasMatchingExpense = true;
|
|
||||||
}
|
|
||||||
// C. Correspondance par mots-clés (seulement sur les dépenses non liées)
|
|
||||||
elseif (!empty($item['mapping_keywords'])) {
|
|
||||||
$keywords = array_map('trim', explode(',', $item['mapping_keywords']));
|
|
||||||
foreach ($unlinkedExpenses as $uexp) {
|
|
||||||
foreach ($keywords as $kw) {
|
|
||||||
if (!empty($kw) && stripos($uexp['label'], $kw) !== false) {
|
|
||||||
$realSum += (float)$uexp['amount'];
|
|
||||||
$hasMatchingExpense = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$realAbs = abs($realSum);
|
|
||||||
$isAutoChecked = ($hasMatchingExpense && ($realAbs >= ($targetAbs - 0.10)));
|
|
||||||
|
|
||||||
$rowClass = ($item['category'] === 'income') ? 'row-income' : 'row-expense';
|
|
||||||
if ($item['is_estimate']) $rowClass .= ' row-estimate';
|
|
||||||
?>
|
?>
|
||||||
|
<tr class="row-income" style="border-bottom:1px solid var(--border-light); background:#f0fdf4;">
|
||||||
|
<td style="padding:15px;">
|
||||||
|
<strong>Apport <?= htmlspecialchars($salary['person']) ?></strong>
|
||||||
|
<span title="Le montant cible est défini dans les paramètres" style="font-size:0.8rem; cursor:help;">⚙️</span>
|
||||||
|
</td>
|
||||||
|
<td class="cell-amount" style="font-weight:600; padding:15px; color:#10b981;">
|
||||||
|
+ <?= number_format($mensualite, 2, ',', ' ') ?> €
|
||||||
|
|
||||||
|
<?php if ($realPerçu > 0): ?>
|
||||||
|
<?php if ($gap > 0.05): ?>
|
||||||
|
<div style="font-size:0.75rem; color:#10b981; font-weight:bold;">Bonus : +<?= number_format($gap, 2, ',', ' ') ?> €</div>
|
||||||
|
<?php elseif ($gap < -0.05): ?>
|
||||||
|
<div style="font-size:0.75rem; color:#f59e0b;">Manque : <?= number_format(abs($gap), 2, ',', ' ') ?> €</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div style="font-size:0.75rem; color:#10b981;">Atteint ✓</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td style="padding:15px;">
|
||||||
|
<span class="badge-type" style="background:#dcfce7; color:#16a34a; padding:4px 8px; border-radius:12px; font-size:0.8rem; font-weight:600;">Revenu Mensuel</span>
|
||||||
|
</td>
|
||||||
|
<td style="padding:15px; color:#64748b; font-weight:bold;">-</td>
|
||||||
|
<td style="padding:15px;">
|
||||||
|
<?php if ($isAutoChecked): ?>
|
||||||
|
<div style="display:inline-flex; align-items:center; gap:5px; background:#dcfce7; color:#16a34a; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #bbf7d0;"><span>✓</span> Reçu</div>
|
||||||
|
<?php elseif ($realPerçu > 0): ?>
|
||||||
|
<div style="display:inline-flex; align-items:center; gap:5px; background:#fffbeb; color:#d97706; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #fde68a;"><span>⏳</span> Partiel</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div style="display:inline-flex; align-items:center; gap:5px; background:#f8fafc; color:#94a3b8; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #e2e8f0;"><span>○</span> En attente</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td style="padding:15px; text-align:right;">
|
||||||
|
<small style="color:#94a3b8;">Via Paramètres</small>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
foreach ($items as $item):
|
||||||
|
// 1. Calculs de base
|
||||||
|
$targetAbs = abs((float)$item['amount']);
|
||||||
|
|
||||||
|
// --- 🚀 INTERCEPTION DYNAMIQUE ---
|
||||||
|
$calcDetails = "";
|
||||||
|
if (isset($item['is_dynamic']) && $item['is_dynamic'] == 1 && !empty($item['dynamic_code'])) {
|
||||||
|
require_once __DIR__ . '/../includes/DynamicBudgetCalculator.php';
|
||||||
|
// Facturation à terme échu : on recule d'un mois pour le calcul
|
||||||
|
$prevDate = date('Y-m-d', strtotime('-1 month', strtotime("$currentYear-$currentMonth-01")));
|
||||||
|
$calcYear = date('Y', strtotime($prevDate));
|
||||||
|
$calcMonth = date('m', strtotime($prevDate));
|
||||||
|
|
||||||
|
$calc = new DynamicBudgetCalculator($pdo, $calcYear, $calcMonth);
|
||||||
|
$estimateData = $calc->getEstimate($item['dynamic_code']);
|
||||||
|
$targetAbs = $estimateData['amount'];
|
||||||
|
$calcDetails = $estimateData['details'];
|
||||||
|
}
|
||||||
|
// --------------------------------
|
||||||
|
|
||||||
|
$amountToAdd = ($item['type'] === 'Annuel') ? $targetAbs / 12 : $targetAbs;
|
||||||
|
$totalDepenses += $amountToAdd;
|
||||||
|
|
||||||
|
// 2. RÉINITIALISATION STRICTE
|
||||||
|
$realSum = 0;
|
||||||
|
$hasMatchingExpense = false;
|
||||||
|
|
||||||
|
// A. Correspondance par ID direct (Le lien dur)
|
||||||
|
if (isset($realTotalsById[$item['id']])) {
|
||||||
|
$realSum = (float)$realTotalsById[$item['id']];
|
||||||
|
$hasMatchingExpense = true;
|
||||||
|
}
|
||||||
|
// B. Correspondance par mots-clés bancaires (Le lien intelligent pour les orphelines)
|
||||||
|
elseif (!empty($item['mapping_keywords'])) {
|
||||||
|
$keywords = array_map('trim', explode(',', $item['mapping_keywords']));
|
||||||
|
foreach ($unlinkedExpenses as $uexp) {
|
||||||
|
foreach ($keywords as $kw) {
|
||||||
|
if (!empty($kw) && stripos($uexp['label'], $kw) !== false) {
|
||||||
|
$realSum += (float)$uexp['amount'];
|
||||||
|
$hasMatchingExpense = true;
|
||||||
|
break; // On a trouvé un mot clé sur cette dépense, on passe à l'orpheline suivante
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// C. Absorption des orphelines par catégories (Le lien Macro/Micro)
|
||||||
|
if (!empty($item['category'])) {
|
||||||
|
$catCodes = array_map('trim', explode(',', $item['category']));
|
||||||
|
foreach ($catCodes as $cCode) {
|
||||||
|
// Si la catégorie existe dans les orphelines et n'a pas encore été consommée
|
||||||
|
if (!empty($cCode) && isset($realTotalsByCat[$cCode]) && $realTotalsByCat[$cCode] != 0) {
|
||||||
|
$realSum += $realTotalsByCat[$cCode];
|
||||||
|
$hasMatchingExpense = true;
|
||||||
|
// On la vide pour ne pas la compter 2 fois si une autre ligne utilise la même catégorie
|
||||||
|
$realTotalsByCat[$cCode] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Finalisation des variables d'affichage
|
||||||
|
$realAbs = abs($realSum);
|
||||||
|
$isAutoChecked = ($hasMatchingExpense && ($realAbs >= ($targetAbs - 0.10)));
|
||||||
|
$rowClass = 'row-expense' . ($item['is_estimate'] ? ' row-estimate' : '');
|
||||||
|
?>
|
||||||
<tr class="<?= $rowClass ?>" style="border-bottom:1px solid var(--border-light);">
|
<tr class="<?= $rowClass ?>" style="border-bottom:1px solid var(--border-light);">
|
||||||
<td style="padding:15px;">
|
<td style="padding:15px;">
|
||||||
<strong><?= htmlspecialchars($item['name']) ?></strong>
|
<strong><?= htmlspecialchars($item['name']) ?></strong>
|
||||||
<?= $item['is_estimate'] ? ' <small style="color:#64748b;">('.tr('bud_est_short').')</small>' : '' ?>
|
<?= $item['is_estimate'] ? ' <small style="color:#64748b;">(Variable)</small>' : '' ?>
|
||||||
|
|
||||||
<?php if(!empty($item['mapping_keywords'])): ?>
|
<?php if(!empty($item['mapping_keywords'])): ?>
|
||||||
<span title="<?= htmlspecialchars($item['mapping_keywords']) ?>" style="font-size:0.7rem; cursor:help;">🔗</span>
|
<span title="Reconnaissance bancaire : <?= htmlspecialchars($item['mapping_keywords']) ?>" style="font-size:0.7rem; cursor:help;">🔗</span>
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if(!empty($item['reg_month'])): ?>
|
|
||||||
<div style="font-size:0.75rem; color:#94a3b8; font-style:italic; margin-top:2px;">
|
|
||||||
📅 <?= sprintf(tr('bud_reg_planned_in'), tr('month_'.str_pad(array_search($item['reg_month'], $moisFr), 2, '0', STR_PAD_LEFT))) ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="cell-amount" style="font-weight:600; padding:15px; color:<?= $item['category']==='income'?'#10b981':'#1e293b' ?>;">
|
<td class="cell-amount" style="font-weight:600; padding:15px; color:#1e293b;">
|
||||||
<?= number_format($targetAbs, 2, ',', ' ') ?> €
|
- <?= number_format($targetAbs, 2, ',', ' ') ?> €
|
||||||
|
|
||||||
|
<?php if (!empty($calcDetails)): ?>
|
||||||
|
<span title="<?= htmlspecialchars($calcDetails) ?>" style="cursor:help; margin-left:4px; font-size:0.85rem;">ℹ️</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if ($hasMatchingExpense): ?>
|
<?php if ($hasMatchingExpense): ?>
|
||||||
<?php
|
<?php $gap = $realAbs - $targetAbs;
|
||||||
// Le "Gap" est la différence visuelle.
|
|
||||||
$gap = $realAbs - $targetAbs;
|
|
||||||
|
|
||||||
if ($gap > 0.05): ?>
|
if ($gap > 0.05): ?>
|
||||||
<div style="font-size:0.75rem; color:#ef4444; font-weight:bold;">
|
<div style="font-size:0.75rem; color:#ef4444; font-weight:bold;">Dépassé : +<?= number_format($gap, 2, ',', ' ') ?> €</div>
|
||||||
<?= $item['category'] === 'income' ? tr('bud_bonus') : tr('bud_overrun') ?> : +<?= number_format($gap, 2, ',', ' ') ?> €
|
|
||||||
</div>
|
|
||||||
<?php elseif ($gap < -0.05): ?>
|
<?php elseif ($gap < -0.05): ?>
|
||||||
<div style="font-size:0.75rem; color:#f59e0b; font-weight:normal;">
|
<div style="font-size:0.75rem; color:#f59e0b;">Reste : <?= number_format(abs($gap), 2, ',', ' ') ?> €</div>
|
||||||
<?= tr('bud_remaining') ?> : <?= number_format(abs($gap), 2, ',', ' ') ?> €
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div style="font-size:0.75rem; color:#10b981; font-weight:normal;">
|
<div style="font-size:0.75rem; color:#10b981;">Atteint ✓</div>
|
||||||
<?= tr('bud_exact_amount') ?> ✓
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php elseif ($item['type'] === 'Annuel'): ?>
|
<?php elseif ($item['type'] === 'Annuel'): ?>
|
||||||
<div style="font-size:0.75rem; color:#94a3b8; font-weight:normal;"><?= tr('bud_per_month_short') ?> <?= number_format($amountToAdd, 2, ',', ' ') ?>/<?= tr('bud_month_short') ?></div>
|
<div style="font-size:0.75rem; color:#94a3b8;"><?= number_format($amountToAdd, 2, ',', ' ') ?>/mois</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td style="padding:15px;">
|
<td style="padding:15px;">
|
||||||
<span class="badge-type <?= strtolower($item['type']) ?>" style="background:#e2e8f0; padding:4px 8px; border-radius:12px; font-size:0.8rem; font-weight:600; color:#475569;">
|
<span class="badge-type" style="background:#e2e8f0; padding:4px 8px; border-radius:12px; font-size:0.8rem; font-weight:600; color:#475569;">
|
||||||
<?= tr('bud_freq_'.strtolower($item['type'])) ?>
|
<?= htmlspecialchars($item['type']) ?>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style="padding:15px; color:#64748b; font-weight:bold;"><?= $item['payment_day'] ? $item['payment_day'] : '-' ?></td>
|
<td style="padding:15px; color:#64748b; font-weight:bold;"><?= $item['payment_day'] ?: '-' ?></td>
|
||||||
|
|
||||||
<td style="padding:15px;">
|
<td style="padding:15px;">
|
||||||
<?php if ($isAutoChecked): ?>
|
<?php if ($isAutoChecked): ?>
|
||||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#f0fdf4; color:#16a34a; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #bbf7d0;">
|
<div style="display:inline-flex; align-items:center; gap:5px; background:#f0fdf4; color:#16a34a; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #bbf7d0;"><span>✓</span> Validé</div>
|
||||||
<span>✓</span> <?= tr('bud_state_validated') ?>
|
|
||||||
</div>
|
|
||||||
<?php elseif($hasMatchingExpense): ?>
|
<?php elseif($hasMatchingExpense): ?>
|
||||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#fffbeb; color:#d97706; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #fde68a;">
|
<div style="display:inline-flex; align-items:center; gap:5px; background:#fffbeb; color:#d97706; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #fde68a;"><span>⏳</span> Partiel</div>
|
||||||
<span>⏳</span> <?= tr('bud_state_partial') ?>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#f8fafc; color:#94a3b8; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #e2e8f0;">
|
<div style="display:inline-flex; align-items:center; gap:5px; background:#f8fafc; color:#94a3b8; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #e2e8f0;"><span>○</span> En attente</div>
|
||||||
<span>○</span> <?= tr('bud_state_waiting') ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td style="padding:15px; text-align:right;">
|
<td style="padding:15px; text-align:right;">
|
||||||
<div class="action-buttons" style="display:flex; gap:5px; justify-content:flex-end;">
|
<div class="action-buttons" style="display:flex; gap:5px; justify-content:flex-end;">
|
||||||
<button class="btn-icon-action edit" onclick='editRecapItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="<?= tr('edit') ?>">✏️</button>
|
<button class="btn-icon-action edit" onclick='editRecapItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="Modifier">✏️</button>
|
||||||
<button class="btn-icon-action delete" onclick="deleteRecapItem(<?= $item['id'] ?>)" title="<?= tr('delete') ?>">🗑️</button>
|
<button class="btn-icon-action delete" onclick="deleteRecapItem(<?= $item['id'] ?>)" title="Supprimer">🗑️</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -189,34 +248,30 @@ $totalRevenus = 0;
|
|||||||
</tbody>
|
</tbody>
|
||||||
<tfoot style="background:#f8fafc;">
|
<tfoot style="background:#f8fafc;">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="1" style="padding:15px;"><strong><?= tr('bud_total_income_smoothed') ?></strong></td>
|
<td colspan="1" style="padding:15px;"><strong>Total Revenus Lissés</strong></td>
|
||||||
<td colspan="5" style="padding:15px; color:#10b981;"><strong>+ <?= number_format($totalRevenus, 2, ',', ' ') ?> €</strong></td>
|
<td colspan="5" style="padding:15px; color:#10b981;"><strong>+ <?= number_format($totalRevenus, 2, ',', ' ') ?> €</strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="1" style="padding:15px;"><strong><?= tr('bud_total_expenses_smoothed') ?></strong></td>
|
<td colspan="1" style="padding:15px;"><strong>Total Dépenses & Estimations</strong></td>
|
||||||
<td colspan="5" style="padding:15px; color:#ef4444;"><strong>- <?= number_format($totalDepenses, 2, ',', ' ') ?> €</strong></td>
|
<td colspan="5" style="padding:15px; color:#ef4444;"><strong>- <?= number_format($totalDepenses, 2, ',', ' ') ?> €</strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr style="border-top: 2px solid #e2e8f0; background: white;">
|
<tr style="border-top: 2px solid #e2e8f0; background: white;">
|
||||||
<td colspan="1" style="padding:15px; font-size:1.1rem;"><strong><?= tr('bud_theoretical_balance_recap') ?></strong></td>
|
<td colspan="1" style="padding:15px; font-size:1.1rem;"><strong>Reste à Vivre (Équilibre)</strong></td>
|
||||||
<?php $balance = $totalRevenus - $totalDepenses; ?>
|
<?php $balance = $totalRevenus - $totalDepenses; ?>
|
||||||
<td colspan="5" style="padding:15px; font-size: 1.3em;" class="<?= $balance >= 0 ? 'text-success' : 'text-danger' ?>">
|
<td colspan="5" style="padding:15px; font-size: 1.3em;">
|
||||||
<strong style="color:<?= $balance >= 0 ? '#10b981' : '#ef4444' ?>;"><?= number_format($balance, 2, ',', ' ') ?> € / <?= tr('bud_month_short') ?></strong>
|
<strong style="color:<?= $balance >= 0 ? '#10b981' : '#ef4444' ?>;"><?= number_format($balance, 2, ',', ' ') ?> € / mois</strong>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="budget-note" style="margin-top:15px; font-size:0.85rem; color:#64748b;">
|
|
||||||
<p>* <?= tr('bud_recap_footer_note') ?></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="budgetRecapModal" class="pf-modal" style="display:none; position:fixed; inset:0; z-index:9999; background:rgba(15, 23, 42, 0.6); backdrop-filter:blur(4px); align-items:center; justify-content:center;">
|
<div id="budgetRecapModal" class="pf-modal" style="display:none; position:fixed; inset:0; z-index:9999; background:rgba(15, 23, 42, 0.6); backdrop-filter:blur(4px); align-items:center; justify-content:center;">
|
||||||
<div class="pf-modal-content" style="background:white; width:95%; max-width:500px; border-radius:20px; box-shadow:0 20px 25px -5px rgba(0,0,0,0.1); padding:30px; position:relative;">
|
<div class="pf-modal-content" style="background:white; width:95%; max-width:500px; border-radius:20px; box-shadow:0 20px 25px -5px rgba(0,0,0,0.1); padding:30px; position:relative;">
|
||||||
|
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||||||
<h3 id="recapModalTitle" style="margin:0; font-size:1.2rem; color:#1e293b;"><?= tr('bud_recap_modal_add') ?></h3>
|
<h3 id="recapModalTitle" style="margin:0; font-size:1.2rem; color:#1e293b;">Ajouter une ligne</h3>
|
||||||
<button type="button" onclick="closeRecapModal()" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">×</button>
|
<button type="button" onclick="closeRecapModal()" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -225,90 +280,66 @@ $totalRevenus = 0;
|
|||||||
<input type="hidden" name="id" id="item_id">
|
<input type="hidden" name="id" id="item_id">
|
||||||
|
|
||||||
<div class="form-group" style="margin-bottom:15px;">
|
<div class="form-group" style="margin-bottom:15px;">
|
||||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_name') ?></label>
|
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Nom de la ligne</label>
|
||||||
<input type="text" name="name" id="item_name" required class="pf-input" placeholder="<?= tr('bud_ph_item_name') ?>" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
|
<input type="text" name="name" id="item_name" required class="pf-input" placeholder="ex: Assurance Auto, Estimation Courses..." style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" style="margin-bottom:15px;">
|
|
||||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_keywords') ?></label>
|
|
||||||
<input type="text" name="mapping_keywords" id="item_keywords" class="pf-input" placeholder="<?= tr('bud_ph_keywords') ?>" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:#f0f9ff; border-color:#bae6fd;">
|
|
||||||
<small style="color:#64748b; font-size:0.75rem;"><?= tr('bud_help_keywords') ?></small>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex; gap:15px; margin-bottom:15px;">
|
<div style="display:flex; gap:15px; margin-bottom:15px;">
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_amount_eur') ?></label>
|
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Montant Prévu (€)</label>
|
||||||
<input type="number" step="0.01" name="amount" id="item_amount" required class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
|
<input type="number" step="0.01" name="amount" id="item_amount" required class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
|
||||||
</div>
|
</div>
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_day') ?></label>
|
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Type de Dépense</label>
|
||||||
<input type="number" min="1" max="31" name="payment_day" id="item_day" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
|
<select name="is_estimate" id="item_is_estimate" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
||||||
|
<option value="0">Fixe (Facture, Prélèvement)</option>
|
||||||
|
<option value="1">Variable (Estimation, Enveloppe)</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex; gap:15px; margin-bottom:15px;">
|
<div style="display:flex; gap:15px; margin-bottom:15px;">
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_category') ?></label>
|
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">
|
||||||
<select name="category" id="item_category" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
Catégorie(s) Cible(s) <small style="font-weight:normal;">(Maintenez Ctrl/Cmd pour choix multiple)</small>
|
||||||
<option value="expense"><?= tr('bud_cat_expense') ?></option>
|
</label>
|
||||||
<option value="income"><?= tr('bud_cat_income') ?></option>
|
<select name="category_multi[]" id="item_category" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white; min-height: 120px;" multiple required>
|
||||||
|
<?php foreach ($dbCategories as $c): ?>
|
||||||
|
<?php if(strtolower($c['type']) === 'expense'): ?>
|
||||||
|
<option value="<?= htmlspecialchars($c['code']) ?>">
|
||||||
|
<?= htmlspecialchars($c['icon'] . ' ' . $c['label']) ?>
|
||||||
|
</option>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_frequency') ?></label>
|
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Fréquence</label>
|
||||||
<select name="type" id="item_type" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
<select name="type" id="item_type" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
||||||
<option value="Mensuel"><?= tr('bud_freq_mensuel') ?></option>
|
<option value="Mensuel">Mensuel</option>
|
||||||
<option value="Annuel"><?= tr('bud_freq_annuel') ?></option>
|
<option value="Annuel">Annuel</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex; gap:15px; margin-bottom:25px;">
|
<div class="form-group" style="margin-bottom:15px;">
|
||||||
<div style="flex:1;">
|
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Mots-clés (Reconnaissance Bancaire)</label>
|
||||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_amount_type') ?></label>
|
<input type="text" name="mapping_keywords" id="item_keywords" class="pf-input" placeholder="ex: NETFLIX, DOMOFINANCE..." style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:#f0f9ff; border-color:#bae6fd;">
|
||||||
<select name="is_estimate" id="item_is_estimate" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
<small style="color:#64748b; font-size:0.75rem;">Optionnel. Permet au système de lier automatiquement un import CSV à cette ligne.</small>
|
||||||
<option value="0"><?= tr('bud_type_fixed') ?></option>
|
|
||||||
<option value="1"><?= tr('bud_type_variable') ?></option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div style="flex:1;">
|
|
||||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_regularization') ?></label>
|
|
||||||
<select name="reg_month" id="item_reg_month" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
|
||||||
<option value=""><?= tr('bud_reg_none') ?></option>
|
|
||||||
<?php
|
|
||||||
foreach($moisFr as $index => $m) {
|
|
||||||
if($index == 0) continue;
|
|
||||||
echo "<option value='$m'>" . tr('month_'.str_pad($index, 2, '0', STR_PAD_LEFT)) . "</option>";
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex; justify-content:flex-end; gap:10px;">
|
<div style="display:flex; justify-content:flex-end; gap:10px; margin-top: 25px;">
|
||||||
<button type="button" onclick="closeRecapModal()" class="pf-btn btn-secondary" style="width:auto; margin:0; background:#f1f5f9; color:#475569; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;"><?= tr('btn_cancel') ?></button>
|
<button type="button" onclick="closeRecapModal()" class="pf-btn btn-secondary" style="width:auto; margin:0; background:#f1f5f9; color:#475569; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;">Annuler</button>
|
||||||
<button type="submit" class="pf-btn" style="width:auto; margin:0; background:#2563eb; color:white; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;"><?= tr('btn_save') ?></button>
|
<button type="submit" class="pf-btn" style="width:auto; margin:0; background:#2563eb; color:white; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;">Enregistrer</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// --- 1. SÉCURISATION TRADUCTIONS ET LANGUE ---
|
|
||||||
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
|
||||||
window.I18N = {
|
|
||||||
...(window.I18N || {}),
|
|
||||||
'bud_recap_modal_add': <?= json_encode(tr('bud_recap_modal_add')) ?>,
|
|
||||||
'bud_recap_modal_edit': <?= json_encode(tr('bud_recap_modal_edit')) ?>,
|
|
||||||
'bud_recap_confirm_delete': <?= json_encode(tr('bud_recap_confirm_delete')) ?>,
|
|
||||||
'bud_err_delete': <?= json_encode(tr('bud_err_delete')) ?>,
|
|
||||||
'bud_err_tech': <?= json_encode(tr('bud_err_tech')) ?>,
|
|
||||||
'bud_saving': <?= json_encode(tr('bud_sav_saving') ?? 'Sauvegarde...') ?>
|
|
||||||
};
|
|
||||||
|
|
||||||
function openRecapModal(mode) {
|
function openRecapModal(mode) {
|
||||||
if (mode === "add") {
|
if (mode === "add") {
|
||||||
document.getElementById("recapModalTitle").innerText = window.I18N['bud_recap_modal_add'] || 'Ajouter';
|
document.getElementById("recapModalTitle").innerText = 'Ajouter une ligne';
|
||||||
document.getElementById("item_id").value = "";
|
document.getElementById("item_id").value = "";
|
||||||
document.getElementById("recapForm").reset();
|
document.getElementById("recapForm").reset();
|
||||||
}
|
}
|
||||||
@@ -322,113 +353,65 @@ function closeRecapModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.onclick = function(event) {
|
window.onclick = function(event) {
|
||||||
const modal = document.getElementById('budgetRecapModal');
|
if (event.target == document.getElementById('budgetRecapModal')) closeRecapModal();
|
||||||
if (event.target == modal) {
|
|
||||||
closeRecapModal();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function editRecapItem(item) {
|
function editRecapItem(item) {
|
||||||
const data = typeof item === "string" ? JSON.parse(item) : item;
|
const data = typeof item === "string" ? JSON.parse(item) : item;
|
||||||
|
document.getElementById("recapModalTitle").innerText = "Editer : " + data.name;
|
||||||
document.getElementById("recapModalTitle").innerText = (window.I18N['bud_recap_modal_edit'] || 'Editer') + " : " + data.name;
|
|
||||||
document.getElementById("item_id").value = data.id;
|
document.getElementById("item_id").value = data.id;
|
||||||
document.getElementById("item_name").value = data.name;
|
document.getElementById("item_name").value = data.name;
|
||||||
document.getElementById("item_keywords").value = data.mapping_keywords || '';
|
document.getElementById("item_keywords").value = data.mapping_keywords || '';
|
||||||
document.getElementById("item_amount").value = Math.abs(data.amount);
|
document.getElementById("item_amount").value = Math.abs(data.amount);
|
||||||
document.getElementById("item_category").value = data.category;
|
const selectedCats = data.category ? data.category.split(',') : [];
|
||||||
|
const selectEl = document.getElementById("item_category");
|
||||||
|
Array.from(selectEl.options).forEach(opt => {
|
||||||
|
opt.selected = selectedCats.includes(opt.value);
|
||||||
|
});
|
||||||
document.getElementById("item_type").value = data.type;
|
document.getElementById("item_type").value = data.type;
|
||||||
document.getElementById("item_day").value = data.payment_day;
|
|
||||||
document.getElementById("item_reg_month").value = data.reg_month || "";
|
|
||||||
document.getElementById("item_is_estimate").value = data.is_estimate;
|
document.getElementById("item_is_estimate").value = data.is_estimate;
|
||||||
|
|
||||||
document.getElementById("budgetRecapModal").style.display = "flex";
|
document.getElementById("budgetRecapModal").style.display = "flex";
|
||||||
document.body.classList.add('no-scroll');
|
document.body.classList.add('no-scroll');
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 2. INTERCEPTION ASYNCHRONE DU FORMULAIRE ---
|
document.getElementById('recapForm').addEventListener('submit', async function(e) {
|
||||||
const recapForm = document.getElementById('recapForm');
|
e.preventDefault();
|
||||||
if (recapForm) {
|
const btn = this.querySelector('button[type="submit"]');
|
||||||
recapForm.addEventListener('submit', async function(e) {
|
btn.disabled = true; btn.innerText = '⏳ ...';
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const submitBtn = this.querySelector('button[type="submit"]');
|
const formData = new FormData(this);
|
||||||
const originalText = submitBtn.innerText;
|
|
||||||
submitBtn.innerText = window.I18N['bud_saving'] || '⏳ ...';
|
|
||||||
submitBtn.disabled = true;
|
|
||||||
|
|
||||||
const formData = new FormData(this);
|
const cats = formData.getAll('category_multi[]');
|
||||||
formData.append('ajax', '1');
|
formData.delete('category_multi[]');
|
||||||
|
formData.append('category', cats.join(','));
|
||||||
|
|
||||||
// 💡 Utilisation sécurisée de getAttribute
|
formData.append('ajax', '1');
|
||||||
const actionUrl = this.getAttribute('action');
|
const url = this.getAttribute('action');
|
||||||
const finalUrl = actionUrl.startsWith('/') ? actionUrl.substring(1) : actionUrl;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(finalUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
||||||
|
|
||||||
// 💡 Lecture robuste (anti-Warnings PHP)
|
|
||||||
const textResult = await response.text();
|
|
||||||
try {
|
|
||||||
const result = JSON.parse(textResult);
|
|
||||||
if (result.success) {
|
|
||||||
closeRecapModal();
|
|
||||||
window.location.reload();
|
|
||||||
} else {
|
|
||||||
alert((window.I18N['bud_err_tech'] || 'Erreur') + " : " + (result.error || "Inconnue"));
|
|
||||||
}
|
|
||||||
} catch (jsonError) {
|
|
||||||
console.error("Réponse non-JSON :", textResult);
|
|
||||||
alert("Le serveur a renvoyé une erreur PHP. Regarde la console (F12).");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur Fetch:", error);
|
|
||||||
alert(window.I18N['bud_err_tech'] || 'Erreur technique');
|
|
||||||
} finally {
|
|
||||||
submitBtn.disabled = false;
|
|
||||||
submitBtn.innerText = originalText;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 3. SUPPRESSION ASYNCHRONE ---
|
|
||||||
async function deleteRecapItem(id) {
|
|
||||||
if (!confirm(window.I18N['bud_recap_confirm_delete'] || "Confirmer la suppression ?")) return;
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("action", "delete");
|
|
||||||
formData.append("id", id);
|
|
||||||
formData.append("ajax", "1"); // Signale à l'API qu'on attend du JSON
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("modules/budget/includes/api/manage-item.php", {
|
const res = await fetch(url.startsWith('/') ? url.substring(1) : url, { method: 'POST', body: formData });
|
||||||
method: "POST",
|
const data = await res.json();
|
||||||
body: formData,
|
if (data.success) window.location.reload();
|
||||||
});
|
else alert("Erreur : " + data.error);
|
||||||
|
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
||||||
|
|
||||||
const textResult = await response.text();
|
|
||||||
try {
|
|
||||||
const result = JSON.parse(textResult);
|
|
||||||
// On s'assure que si l'API ne renvoie pas success, on le signale
|
|
||||||
if (result.success !== false) {
|
|
||||||
window.location.reload();
|
|
||||||
} else {
|
|
||||||
alert((window.I18N['bud_err_delete'] || 'Erreur de suppression') + " : " + (result.error || ""));
|
|
||||||
}
|
|
||||||
} catch (jsonErr) {
|
|
||||||
console.error("Réponse non-JSON lors de la suppression :", textResult);
|
|
||||||
window.location.reload(); // Fallback si l'API redirige au lieu de répondre en JSON
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Erreur réseau Suppression:", err);
|
alert("Erreur technique lors de la sauvegarde.");
|
||||||
alert(window.I18N['bud_err_delete'] || 'Erreur réseau');
|
} finally {
|
||||||
|
btn.disabled = false; btn.innerText = 'Enregistrer';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function deleteRecapItem(id) {
|
||||||
|
if (!confirm("Confirmer la suppression ?")) return;
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("action", "delete"); formData.append("id", id); formData.append("ajax", "1");
|
||||||
|
try {
|
||||||
|
const res = await fetch("modules/budget/includes/api/manage-item.php", { method: "POST", body: formData });
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success !== false) window.location.reload();
|
||||||
|
else alert("Erreur : " + data.error);
|
||||||
|
} catch (err) {
|
||||||
|
window.location.reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -0,0 +1,660 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @var bool $hasAccounts
|
||||||
|
* @var bool $hasCategories
|
||||||
|
* @var bool $hasSalaries
|
||||||
|
* @var bool $hasBudgetItems
|
||||||
|
* @var bool $isBudgetSetupOk
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Sécurité : Ce fichier ne doit pas être appelé directement
|
||||||
|
if (!defined('CURRENCY')) {
|
||||||
|
define('CURRENCY', '€');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="budget-settings-backdrop" id="modal-budget-settings">
|
||||||
|
<div class="budget-settings-modal">
|
||||||
|
<div class="bs-header">
|
||||||
|
<h3>⚙️ <?= tr('budget_settings_title') ?></h3>
|
||||||
|
<button class="bs-close" onclick="closeBudgetSettings()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="bs-layout">
|
||||||
|
<div class="bs-sidebar">
|
||||||
|
<button class="bs-tab-btn active" onclick="switchBsTab('accounts', this)">
|
||||||
|
<?= tr('bs_tab_accounts') ?>
|
||||||
|
<?php if (!$hasAccounts): ?><span class="alert-dot alert-dot-inline">!</span><?php endif; ?>
|
||||||
|
</button>
|
||||||
|
<button class="bs-tab-btn" onclick="switchBsTab('categories', this)">
|
||||||
|
<?= tr('bs_tab_categories') ?>
|
||||||
|
<?php if (!$hasCategories): ?><span class="alert-dot alert-dot-inline">!</span><?php endif; ?>
|
||||||
|
</button>
|
||||||
|
<button class="bs-tab-btn" onclick="switchBsTab('rules', this)"><?= tr('bs_tab_rules') ?></button>
|
||||||
|
<button class="bs-tab-btn" onclick="switchBsTab('salaries', this)">
|
||||||
|
<?= tr('bs_tab_salaries') ?>
|
||||||
|
<?php if (!$hasSalaries): ?><span class="alert-dot alert-dot-inline">!</span><?php endif; ?>
|
||||||
|
</button>
|
||||||
|
<button class="bs-tab-btn" onclick="switchBsTab('csv', this)"><?= tr('bs_tab_csv') ?></button>
|
||||||
|
<button class="bs-tab-btn" onclick="switchBsTab('children', this)">👶 <?= tr('bud_settings_dyn_title') ?></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bs-content">
|
||||||
|
|
||||||
|
<?php if (!isset($isBudgetSetupOk) || !$isBudgetSetupOk || !$hasBudgetItems): ?>
|
||||||
|
<div class="bs-onboarding-card">
|
||||||
|
<h4 class="bs-onboarding-title">
|
||||||
|
<span>🚀</span> <?= tr('bs_onb_title') ?>
|
||||||
|
</h4>
|
||||||
|
<p class="bs-onboarding-desc"><?= tr('bs_onb_desc') ?></p>
|
||||||
|
<ul class="bs-onboarding-list">
|
||||||
|
<li>
|
||||||
|
<strong class="<?= $hasCategories ? 'text-success' : 'text-danger' ?>"><?= $hasCategories ? '✅' : '❌' ?></strong>
|
||||||
|
<a class="bs-onboarding-link" onclick="switchBsTab('categories', document.querySelectorAll('.bs-tab-btn')[1]); return false;"><?= tr('bs_onb_cat_link') ?></a> : <?= tr('bs_onb_cat_desc') ?>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong class="<?= $hasAccounts ? 'text-success' : 'text-danger' ?>"><?= $hasAccounts ? '✅' : '❌' ?></strong>
|
||||||
|
<a class="bs-onboarding-link" onclick="switchBsTab('accounts', document.querySelectorAll('.bs-tab-btn')[0]); return false;"><?= tr('bs_onb_acc_link') ?></a> : <?= tr('bs_onb_acc_desc') ?>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong class="<?= $hasSalaries ? 'text-success' : 'text-danger' ?>"><?= $hasSalaries ? '✅' : '❌' ?></strong>
|
||||||
|
<a class="bs-onboarding-link" onclick="switchBsTab('salaries', document.querySelectorAll('.bs-tab-btn')[3]); return false;"><?= tr('bs_onb_sal_link') ?></a> : <?= tr('bs_onb_sal_desc') ?>
|
||||||
|
</li>
|
||||||
|
<li style="margin-top: 8px; padding-top: 8px; border-top: 1px dashed rgba(180, 83, 9, 0.3);">
|
||||||
|
<strong class="<?= $hasBudgetItems ? 'text-success' : 'text-warning' ?>" style="color: <?= $hasBudgetItems ? '' : '#f59e0b' ?>"><?= $hasBudgetItems ? '✅' : '⏳' ?></strong>
|
||||||
|
<a href="?tab=recap" onclick="closeBudgetSettings();" class="bs-onboarding-link"><?= tr('bs_onb_items_link') ?></a> : <?= tr('bs_onb_items_desc') ?>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div id="pane-accounts" class="bs-pane active">
|
||||||
|
<div class="bs-pane-header">
|
||||||
|
<h4 class="bs-section-title"><?= tr('bs_tab_accounts') ?></h4>
|
||||||
|
<div class="bs-currency-selector">
|
||||||
|
<span><?= tr('bs_currency_label') ?> :</span>
|
||||||
|
<select id="bs-currency-select" class="pf-input bs-currency-select" onchange="updateBudgetCurrency(this)">
|
||||||
|
<option value="€">EUR (€)</option>
|
||||||
|
<option value="$">USD ($)</option>
|
||||||
|
<option value="£">GBP (£)</option>
|
||||||
|
<option value="CHF">CHF</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="bs-list-accounts" class="bs-list-container">⏳ <?= tr('loading') ?></div>
|
||||||
|
<hr class="bs-divider">
|
||||||
|
<h5 class="bs-section-subtitle"><?= tr('bs_add_account_title') ?></h5>
|
||||||
|
<form id="form-add-account" class="bs-form-inline">
|
||||||
|
<input type="text" name="name" class="pf-input bs-input-flex" placeholder="<?= tr('bs_account_name_ph') ?>" required>
|
||||||
|
<select name="type" class="pf-input bs-input-fixed">
|
||||||
|
<option value="checking"><?= tr('bs_type_checking') ?></option>
|
||||||
|
<option value="savings"><?= tr('bs_type_savings') ?></option>
|
||||||
|
</select>
|
||||||
|
<select name="owner_person_id" id="select-account-owner" class="pf-input bs-input-fixed">
|
||||||
|
<option value="">Commun</option>
|
||||||
|
</select>
|
||||||
|
<button type="submit" class="btn btn-secondary"><?= tr('btn_add') ?></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pane-categories" class="bs-pane">
|
||||||
|
<h4 class="bs-section-title"><?= tr('bs_tab_categories') ?></h4>
|
||||||
|
<div id="bs-list-categories" class="bs-list-container">⏳ <?= tr('loading') ?></div>
|
||||||
|
<hr class="bs-divider">
|
||||||
|
<h5 class="bs-section-subtitle"><?= tr('bs_add_category_title') ?></h5>
|
||||||
|
<form id="form-add-category" class="bs-form-inline align-center">
|
||||||
|
<input type="text" name="code" class="pf-input bs-input-fixed" placeholder="<?= tr('bs_cat_code_ph') ?>" required>
|
||||||
|
<input type="text" name="label" class="pf-input bs-input-flex" placeholder="<?= tr('bs_cat_label_ph') ?>" required>
|
||||||
|
|
||||||
|
<select name="type" class="pf-input bs-input-fixed">
|
||||||
|
<option value="Expense"><?= tr('bs_type_expense') ?></option>
|
||||||
|
<option value="Income"><?= tr('bs_type_income') ?></option>
|
||||||
|
<option value="Savings"><?= tr('bs_type_savings_cat') ?></option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select name="icon" class="pf-input bs-input-icon">
|
||||||
|
<option value="📌">📌</option><option value="🛒">🛒</option><option value="🥖">🥖</option>
|
||||||
|
<option value="🍽️">🍽️</option><option value="⛽">⛽</option><option value="🚗">🚗</option>
|
||||||
|
<option value="🚆">🚆</option><option value="🏠">🏠</option><option value="⚡">⚡</option>
|
||||||
|
<option value="💧">💧</option><option value="⚕️">⚕️</option><option value="🎒">🎒</option>
|
||||||
|
<option value="👕">👕</option><option value="📱">📱</option><option value="🎮">🎮</option>
|
||||||
|
<option value="✈️">✈️</option><option value="🎁">🎁</option><option value="🐶">🐶</option>
|
||||||
|
<option value="💵">💵</option><option value="🐷">🐷</option><option value="📈">📈</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input type="color" name="color" value="#3b82f6" class="bs-input-color">
|
||||||
|
<button type="submit" class="btn btn-secondary"><?= tr('btn_add') ?></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pane-rules" class="bs-pane">
|
||||||
|
<h4 class="bs-section-title"><?= tr('bs_tab_rules') ?></h4>
|
||||||
|
<input type="text" id="input-search-rules" class="bs-rule-search" placeholder="<?= tr('bs_rule_search_ph') ?>" onkeyup="filterRules()">
|
||||||
|
<div id="bs-list-rules" class="bs-list-container">⏳ <?= tr('loading') ?></div>
|
||||||
|
<hr class="bs-divider">
|
||||||
|
<h5 class="bs-section-subtitle"><?= tr('bs_add_rule_title') ?></h5>
|
||||||
|
<form id="form-add-rule" class="bs-form-inline align-center">
|
||||||
|
<input type="text" name="keyword" class="pf-input bs-input-flex" placeholder="<?= tr('bs_rule_keyword_ph') ?>" required>
|
||||||
|
<select name="category" id="select-rule-category" class="pf-input bs-input-fixed" required></select>
|
||||||
|
<button type="submit" class="btn btn-secondary"><?= tr('btn_add') ?></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pane-salaries" class="bs-pane">
|
||||||
|
<h4 class="bs-section-title"><?= tr('bs_tab_salaries') ?> (<?= date('Y') ?>)</h4>
|
||||||
|
<div id="bs-list-salaries" class="bs-list-container">⏳ <?= tr('loading') ?></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pane-csv" class="bs-pane">
|
||||||
|
<h4 class="bs-section-title"><?= tr('bs_csv_title') ?></h4>
|
||||||
|
<p class="pf-muted-tiny bs-desc"><?= tr('bs_csv_desc') ?></p>
|
||||||
|
|
||||||
|
<div id="csv-drop-zone" class="bs-csv-dropzone">
|
||||||
|
<div class="bs-csv-icon">📥</div>
|
||||||
|
<h5 class="bs-csv-title"><?= tr('bs_csv_drop_title') ?></h5>
|
||||||
|
<p class="pf-muted-tiny bs-csv-subtitle"><?= tr('bs_csv_drop_desc') ?></p>
|
||||||
|
<input type="file" id="csv_file_input" accept=".csv" style="display: none;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="csv-preview-container" class="bs-csv-preview"></div>
|
||||||
|
|
||||||
|
<h5 class="bs-section-subtitle bordered"><?= tr('bs_csv_settings_title') ?></h5>
|
||||||
|
<form id="form-csv-mapping" onsubmit="saveCsvMapping(event)">
|
||||||
|
<div class="bs-grid-2">
|
||||||
|
<div>
|
||||||
|
<label class="pf-label"><?= tr('bs_csv_delimiter') ?></label>
|
||||||
|
<select name="csv_delimiter" id="csv_delimiter" class="pf-input">
|
||||||
|
<option value=";"><?= tr('bs_csv_delim_semi') ?></option>
|
||||||
|
<option value=","><?= tr('bs_csv_delim_comma') ?></option>
|
||||||
|
<option value="\t"><?= tr('bs_csv_delim_tab') ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="pf-label"><?= tr('bs_csv_date_format') ?></label>
|
||||||
|
<select name="csv_date_format" id="csv_date_format" class="pf-input">
|
||||||
|
<option value="d/m/Y">JJ/MM/AAAA</option>
|
||||||
|
<option value="Y-m-d">AAAA-MM-JJ</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="bs-section-subtitle">
|
||||||
|
<?= tr('bs_csv_col_index') ?>
|
||||||
|
<small class="bs-text-muted-normal"><?= tr('bs_csv_col_index_help') ?></small>
|
||||||
|
</h5>
|
||||||
|
<div class="bs-grid-2">
|
||||||
|
<div>
|
||||||
|
<label class="pf-label"><?= tr('bs_csv_col_date') ?></label>
|
||||||
|
<input type="number" min="0" name="csv_col_date" id="csv_col_date" class="pf-input" value="0" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="pf-label"><?= tr('bs_csv_col_label') ?></label>
|
||||||
|
<input type="number" min="0" name="csv_col_label" id="csv_col_label" class="pf-input" value="1" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bs-card-gray">
|
||||||
|
<label class="pf-label"><?= tr('bs_csv_amount_mgmt') ?></label>
|
||||||
|
<select name="csv_amount_type" id="csv_amount_type" class="pf-input bs-input-mb" onchange="toggleCsvAmountCols(this.value)">
|
||||||
|
<option value="single"><?= tr('bs_csv_amount_single') ?></option>
|
||||||
|
<option value="split"><?= tr('bs_csv_amount_split') ?></option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div class="bs-grid-2">
|
||||||
|
<div>
|
||||||
|
<label class="pf-label" id="lbl_col_debit"><?= tr('bs_csv_col_amount') ?></label>
|
||||||
|
<input type="number" min="0" name="csv_col_debit" id="csv_col_debit" class="pf-input" value="8" required>
|
||||||
|
</div>
|
||||||
|
<div id="wrapper_col_credit" style="display:none;">
|
||||||
|
<label class="pf-label"><?= tr('bs_csv_col_credit') ?></label>
|
||||||
|
<input type="number" min="0" name="csv_col_credit" id="csv_col_credit" class="pf-input" value="9">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bs-input-mb">
|
||||||
|
<label class="pf-label"><?= tr('bs_csv_col_ref') ?></label>
|
||||||
|
<input type="number" min="0" name="csv_col_ref" id="csv_col_ref" class="pf-input" value="3">
|
||||||
|
</div>
|
||||||
|
<div class="bs-text-right">
|
||||||
|
<button type="submit" class="btn btn-secondary">💾 <?= tr('btn_save_format') ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- NOUVEAU PANE ENFANTS -->
|
||||||
|
<div id="pane-children" class="bs-pane">
|
||||||
|
<h4 class="bs-section-title">👶 <?= tr('bud_settings_dyn_title') ?></h4>
|
||||||
|
<p class="pf-muted-tiny bs-desc">Configurez ici les tarifs pour le calcul dynamique des frais de garde et de cantine.</p>
|
||||||
|
|
||||||
|
<form id="form-dynamic-estimates" onsubmit="saveDynamicEstimates(event)">
|
||||||
|
<h5 class="bs-section-subtitle bordered">Assistante Maternelle</h5>
|
||||||
|
<div class="bs-grid-2">
|
||||||
|
<div>
|
||||||
|
<label class="pf-label"><?= tr('bud_dyn_nanny_fixed') ?></label>
|
||||||
|
<input type="number" step="0.01" name="budget_nanny_fixed" id="budget_nanny_fixed" class="pf-input" value="700">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="pf-label"><?= tr('bud_dyn_nanny_daily') ?></label>
|
||||||
|
<input type="number" step="0.01" name="budget_nanny_daily" id="budget_nanny_daily" class="pf-input" value="4">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="pf-label"><?= tr('bud_dyn_nanny_aid') ?></label>
|
||||||
|
<input type="number" step="0.01" name="budget_nanny_aid" id="budget_nanny_aid" class="pf-input" value="120">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="pf-label"><?= tr('bud_dyn_nanny_cesu_avg') ?></label>
|
||||||
|
<input type="number" step="0.01" name="budget_nanny_cesu_avg" id="budget_nanny_cesu_avg" class="pf-input" value="170">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="bs-section-subtitle bordered" style="margin-top: 20px;">École & Centre de loisirs</h5>
|
||||||
|
<div class="bs-grid-2">
|
||||||
|
<div>
|
||||||
|
<label class="pf-label"><?= tr('bud_dyn_school_meal') ?></label>
|
||||||
|
<input type="number" step="0.01" name="budget_school_meal" id="budget_school_meal" class="pf-input" value="5.62">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="pf-label"><?= tr('bud_dyn_school_aftercare') ?></label>
|
||||||
|
<input type="number" step="0.01" name="budget_school_aftercare" id="budget_school_aftercare" class="pf-input" value="1.97">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="pf-label"><?= tr('bud_dyn_school_fullday') ?></label>
|
||||||
|
<input type="number" step="0.01" name="budget_school_fullday" id="budget_school_fullday" class="pf-input" value="21.89">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bs-text-right" style="margin-top: 20px;">
|
||||||
|
<button type="submit" class="btn btn-secondary">💾 <?= tr('btn_save') ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const tabsContainer = document.querySelector('.budget-tabs-container');
|
||||||
|
const activeTab = document.querySelector('.tab-item.active');
|
||||||
|
if (tabsContainer && activeTab) {
|
||||||
|
if (tabsContainer.scrollWidth > tabsContainer.clientWidth) {
|
||||||
|
const scrollPos = activeTab.offsetLeft - (tabsContainer.offsetWidth / 2) + (activeTab.offsetWidth / 2);
|
||||||
|
tabsContainer.scrollTo({ left: scrollPos, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function openBudgetSettings() {
|
||||||
|
document.getElementById('modal-budget-settings').classList.add('show');
|
||||||
|
document.body.classList.add('no-scroll');
|
||||||
|
loadBudgetSettingsData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeBudgetSettings() {
|
||||||
|
document.getElementById('modal-budget-settings').classList.remove('show');
|
||||||
|
document.body.classList.remove('no-scroll');
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('modal-budget-settings')?.addEventListener('click', (e) => {
|
||||||
|
if (e.target.id === 'modal-budget-settings') closeBudgetSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
function switchBsTab(tabId, btnEl) {
|
||||||
|
document.querySelectorAll('.bs-tab-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.bs-pane').forEach(p => p.classList.remove('active'));
|
||||||
|
btnEl.classList.add('active');
|
||||||
|
document.getElementById('pane-' + tabId).classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBudgetSettingsData() {
|
||||||
|
try {
|
||||||
|
const response = await pachaFetch('/modules/budget/includes/api/settings.php?action=get_all', { method: 'GET' });
|
||||||
|
if (!response.success) throw new Error(response.error || tr('error_occured'));
|
||||||
|
|
||||||
|
// Alimenter la liste des propriétaires pour les comptes
|
||||||
|
const ownerSelect = document.getElementById('select-account-owner');
|
||||||
|
if (ownerSelect && response.data.people) {
|
||||||
|
ownerSelect.innerHTML = '<option value="">Commun</option>';
|
||||||
|
response.data.people.forEach(p => {
|
||||||
|
ownerSelect.innerHTML += `<option value="${p.id}">${p.name}</option>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAccounts(response.data.accounts);
|
||||||
|
renderCategories(response.data.categories);
|
||||||
|
populateRuleCategories(response.data.categories);
|
||||||
|
renderRules(response.data.rules);
|
||||||
|
renderSalaries(response.data.salaries, response.data.year);
|
||||||
|
const currencySelect = document.getElementById('bs-currency-select');
|
||||||
|
if (currencySelect && response.data.currency) currencySelect.value = response.data.currency;
|
||||||
|
|
||||||
|
const bs = response.data.budget_settings || {};
|
||||||
|
if (document.getElementById('budget_nanny_fixed')) document.getElementById('budget_nanny_fixed').value = bs.budget_nanny_fixed || 700;
|
||||||
|
if (document.getElementById('budget_nanny_daily')) document.getElementById('budget_nanny_daily').value = bs.budget_nanny_daily || 4;
|
||||||
|
if (document.getElementById('budget_nanny_aid')) document.getElementById('budget_nanny_aid').value = bs.budget_nanny_aid || 120;
|
||||||
|
if (document.getElementById('budget_nanny_cesu_avg')) document.getElementById('budget_nanny_cesu_avg').value = bs.budget_nanny_cesu_avg || 170;
|
||||||
|
if (document.getElementById('budget_school_meal')) document.getElementById('budget_school_meal').value = bs.budget_school_meal || 5.62;
|
||||||
|
if (document.getElementById('budget_school_aftercare')) document.getElementById('budget_school_aftercare').value = bs.budget_school_aftercare || 1.97;
|
||||||
|
if (document.getElementById('budget_school_fullday')) document.getElementById('budget_school_fullday').value = bs.budget_school_fullday || 21.89;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erreur chargement paramètres :", err);
|
||||||
|
const errorMsg = `<div class="pf-alert pf-alert--error bs-input-mb">❌ ${tr('error_loading_settings')} : ${err.message}</div>`;
|
||||||
|
['accounts', 'categories', 'rules', 'salaries'].forEach(id => {
|
||||||
|
const el = document.getElementById('bs-list-' + id);
|
||||||
|
if (el) el.innerHTML = errorMsg;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. ONGLET : COMPTES
|
||||||
|
function renderAccounts(accounts) {
|
||||||
|
const container = document.getElementById('bs-list-accounts');
|
||||||
|
if (!container) return;
|
||||||
|
if (!accounts || accounts.length === 0) {
|
||||||
|
container.innerHTML = `<em class="text-muted">${tr('bs_empty_accounts')}</em>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let html = '';
|
||||||
|
accounts.forEach(acc => {
|
||||||
|
const typeBadge = acc.account_type === 'checking' ? tr('bs_type_checking') : tr('bs_type_savings');
|
||||||
|
const defaultBadge = acc.is_default == 1 ? `<span class="bs-badge-default">${tr('bs_badge_default')}</span>` : '';
|
||||||
|
const ownerText = acc.owner_name ? `${tr('bs_owner')} : ${acc.owner_name}` : tr('bs_shared_account');
|
||||||
|
html += `
|
||||||
|
<div class="bs-list-item">
|
||||||
|
<div>
|
||||||
|
<strong>${acc.name}</strong> ${defaultBadge}<br>
|
||||||
|
<small class="text-muted">${typeBadge} · ${ownerText}</small>
|
||||||
|
</div>
|
||||||
|
<div class="pf-flex-gap-8">
|
||||||
|
<button type="button" class="btn-icon-action delete" onclick="deleteAccount(${acc.id})" title="${tr('btn_delete')}">🗑️</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. ONGLET : CATÉGORIES
|
||||||
|
function renderCategories(categories) {
|
||||||
|
const container = document.getElementById('bs-list-categories');
|
||||||
|
if (!container) return;
|
||||||
|
if (!categories || categories.length === 0) {
|
||||||
|
container.innerHTML = `<em class="text-muted">${tr('bs_empty_categories')}</em>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tri JS pour respecter l'ordre au chargement de la modale
|
||||||
|
categories.sort((a, b) => {
|
||||||
|
if (a.type !== b.type) return a.type === 'Income' ? -1 : 1;
|
||||||
|
return (a.sort_order || 0) - (b.sort_order || 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
categories.forEach(cat => {
|
||||||
|
let typeLabel = cat.type === 'Income' ? tr('bs_type_income') : cat.type === 'Expense' ? tr('bs_type_expense') : tr('bs_type_savings_cat');
|
||||||
|
html += `
|
||||||
|
<div class="bs-list-item" data-id="${cat.id}" style="border-left: 4px solid ${cat.color || '#ccc'};">
|
||||||
|
<div>
|
||||||
|
<span style="font-size:1.2rem; margin-right:8px;">${cat.icon || '📌'}</span>
|
||||||
|
<strong>${cat.label}</strong> <small class="text-muted">(${cat.code} — ${typeLabel})</small>
|
||||||
|
</div>
|
||||||
|
<div class="pf-flex-gap-8">
|
||||||
|
<button type="button" class="btn-icon-action" onclick="moveCatUp(this)" title="Monter">⬆️</button>
|
||||||
|
<button type="button" class="btn-icon-action" onclick="moveCatDown(this)" title="Descendre">⬇️</button>
|
||||||
|
<button type="button" class="btn-icon-action delete" onclick="deleteBudgetCategory(${cat.id})" title="${tr('btn_delete')}">🗑️</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nouvelles fonctions pour réordonner visuellement et sauvegarder
|
||||||
|
function moveCatUp(btn) {
|
||||||
|
let row = btn.closest('.bs-list-item');
|
||||||
|
if (row.previousElementSibling) {
|
||||||
|
row.parentNode.insertBefore(row, row.previousElementSibling);
|
||||||
|
saveCategoriesOrder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveCatDown(btn) {
|
||||||
|
let row = btn.closest('.bs-list-item');
|
||||||
|
if (row.nextElementSibling) {
|
||||||
|
row.parentNode.insertBefore(row.nextElementSibling, row);
|
||||||
|
saveCategoriesOrder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveCategoriesOrder() {
|
||||||
|
let ids = Array.from(document.querySelectorAll('#bs-list-categories .bs-list-item')).map(el => el.dataset.id);
|
||||||
|
let fd = new FormData();
|
||||||
|
fd.append('action', 'save_categories_order');
|
||||||
|
fd.append('order', JSON.stringify(ids));
|
||||||
|
try {
|
||||||
|
await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erreur de sauvegarde de l'ordre :", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. ONGLET : RÈGLES D'IMPORT
|
||||||
|
function renderRules(rules) {
|
||||||
|
const container = document.getElementById('bs-list-rules');
|
||||||
|
if (!container) return;
|
||||||
|
if (!rules || rules.length === 0) {
|
||||||
|
container.innerHTML = `<em class="text-muted">${tr('bs_empty_rules')}</em>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const groupedRules = {};
|
||||||
|
rules.forEach(r => {
|
||||||
|
const catName = r.cat_label || r.category;
|
||||||
|
if (!groupedRules[catName]) groupedRules[catName] = [];
|
||||||
|
groupedRules[catName].push(r);
|
||||||
|
});
|
||||||
|
let html = '';
|
||||||
|
for (const [catName, catRules] of Object.entries(groupedRules)) {
|
||||||
|
html += `
|
||||||
|
<details class="pf-accordion js-rule-group" data-catname="${catName.toLowerCase()}">
|
||||||
|
<summary class="pf-accordion-summary bs-accordion-summary">
|
||||||
|
<span>📁 ${catName}</span>
|
||||||
|
<span class="bs-accordion-count">${catRules.length}</span>
|
||||||
|
</summary>
|
||||||
|
<div class="pf-accordion-content" style="padding: 0.5rem;">
|
||||||
|
<div class="bs-rule-tags">`;
|
||||||
|
catRules.forEach(r => {
|
||||||
|
html += `<div class="bs-rule-tag js-rule-tag" data-keyword="${r.keyword.toLowerCase()}">
|
||||||
|
${r.keyword}
|
||||||
|
<button type="button" class="bs-rule-tag-del" onclick="deleteRule(${r.id})" title="${tr('btn_delete')}">×</button>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
html += ` </div>
|
||||||
|
</div>
|
||||||
|
</details>`;
|
||||||
|
}
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterRules() {
|
||||||
|
const searchVal = document.getElementById('input-search-rules').value.toLowerCase();
|
||||||
|
document.querySelectorAll('.js-rule-group').forEach(group => {
|
||||||
|
let hasVisibleTag = false;
|
||||||
|
const catName = group.getAttribute('data-catname');
|
||||||
|
group.querySelectorAll('.js-rule-tag').forEach(tag => {
|
||||||
|
const keyword = tag.getAttribute('data-keyword');
|
||||||
|
if (keyword.includes(searchVal)) { tag.style.display = 'inline-flex'; hasVisibleTag = true; }
|
||||||
|
else { tag.style.display = 'none'; }
|
||||||
|
});
|
||||||
|
if (catName.includes(searchVal) && searchVal.length > 0) {
|
||||||
|
group.querySelectorAll('.js-rule-tag').forEach(tag => tag.style.display = 'inline-flex');
|
||||||
|
hasVisibleTag = true;
|
||||||
|
}
|
||||||
|
if (hasVisibleTag) {
|
||||||
|
group.style.display = 'block';
|
||||||
|
if (searchVal.length > 0) group.setAttribute('open', '');
|
||||||
|
} else {
|
||||||
|
group.style.display = 'none'; group.removeAttribute('open');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. ONGLET : SALAIRES
|
||||||
|
function renderSalaries(salaries, year) {
|
||||||
|
const container = document.getElementById('bs-list-salaries');
|
||||||
|
if (!container) return;
|
||||||
|
if (!salaries || salaries.length === 0) {
|
||||||
|
container.innerHTML = `<em class="text-muted">${tr('bs_empty_salaries')} ${year}.</em>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currency = '<?= CURRENCY ?>';
|
||||||
|
let html = `<p class="bs-desc">${tr('bs_salaries_year_desc')} <strong>${year}</strong>.</p>`;
|
||||||
|
salaries.forEach(s => {
|
||||||
|
html += `
|
||||||
|
<div class="bs-list-item" id="salary-item-${s.id}">
|
||||||
|
<div>
|
||||||
|
<strong>${s.person}</strong><br>
|
||||||
|
<small class="text-muted">
|
||||||
|
${tr('bs_salary_net')} : ${s.salary} ${currency}<br>
|
||||||
|
${tr('bs_salary_fees')} : ${s.mensualite} ${currency}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="pf-flex-gap-8">
|
||||||
|
<button type="button" class="btn-icon-action edit" data-id="${s.id}" data-person="${s.person}" data-salary="${s.salary}" data-mensualite="${s.mensualite}" onclick="inlineEditSalary(this)" title="${tr('btn_edit')}">✏️</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CRUD ACTIONS ---
|
||||||
|
async function deleteAccount(id) { if (!await pachaConfirm(tr('btn_delete'), tr('bs_confirm_delete'))) return; try { const fd = new FormData(); fd.append('action', 'delete_account'); fd.append('id', id); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); await loadBudgetSettingsData(); } catch (err) { if (typeof showToast === 'function') showToast(err.message, 'error'); else alert("Erreur : " + err.message); } }
|
||||||
|
|
||||||
|
const formAddAccount = document.getElementById('form-add-account');
|
||||||
|
if (formAddAccount) formAddAccount.addEventListener('submit', async (e) => { e.preventDefault(); const btn = formAddAccount.querySelector('button'); const oldText = btn.innerText; btn.innerText = '⏳'; btn.disabled = true; try { const fd = new FormData(formAddAccount); fd.append('action', 'add_account'); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); formAddAccount.reset(); await loadBudgetSettingsData(); if (typeof showToast === 'function') showToast("Compte ajouté", 'success'); } catch (err) { if (typeof showToast === 'function') showToast(err.message, 'error'); else alert("Erreur : " + err.message); } finally { btn.innerText = oldText; btn.disabled = false; } });
|
||||||
|
|
||||||
|
async function deleteBudgetCategory(id) {
|
||||||
|
if (!await pachaConfirm(tr('btn_delete'), tr('bs_confirm_delete_cat'))) return;
|
||||||
|
try {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', 'delete_category');
|
||||||
|
fd.append('id', id);
|
||||||
|
const res = await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd });
|
||||||
|
|
||||||
|
if (!res.success) throw new Error(res.error || tr('error_occured'));
|
||||||
|
|
||||||
|
await loadBudgetSettingsData();
|
||||||
|
} catch (err) {
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast(err.message, 'error');
|
||||||
|
} else {
|
||||||
|
alert("Erreur : " + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formAddCategory = document.getElementById('form-add-category');
|
||||||
|
if (formAddCategory) {
|
||||||
|
formAddCategory.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = formAddCategory.querySelector('button');
|
||||||
|
const oldText = btn.innerText;
|
||||||
|
btn.innerText = '⏳';
|
||||||
|
btn.disabled = true;
|
||||||
|
try {
|
||||||
|
const fd = new FormData(formAddCategory);
|
||||||
|
fd.append('action', 'add_category');
|
||||||
|
const res = await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd });
|
||||||
|
if (!res.success) throw new Error(res.error || "Erreur lors de l'ajout");
|
||||||
|
formAddCategory.reset();
|
||||||
|
await loadBudgetSettingsData();
|
||||||
|
if (typeof showToast === 'function') showToast("Catégorie ajoutée", "success");
|
||||||
|
} catch (err) {
|
||||||
|
if (typeof showToast === 'function') showToast(err.message, 'error'); else alert("Erreur : " + err.message);
|
||||||
|
} finally {
|
||||||
|
btn.innerText = oldText;
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateRuleCategories(categories) { const select = document.getElementById('select-rule-category'); if (!select) return; select.innerHTML = ''; if (!categories) return; categories.forEach(cat => { const opt = document.createElement('option'); opt.value = cat.code; opt.textContent = `${cat.icon || ''} ${cat.label}`; select.appendChild(opt); }); }
|
||||||
|
|
||||||
|
async function deleteRule(id) { if (!await pachaConfirm(tr('btn_delete'), tr('bs_confirm_delete_rule'))) return; try { const fd = new FormData(); fd.append('action', 'delete_rule'); fd.append('id', id); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); await loadBudgetSettingsData(); } catch (err) { if (typeof showToast === 'function') showToast(err.message, 'error'); else alert("Erreur : " + err.message); } }
|
||||||
|
|
||||||
|
const formAddRule = document.getElementById('form-add-rule');
|
||||||
|
if (formAddRule) formAddRule.addEventListener('submit', async (e) => { e.preventDefault(); const btn = formAddRule.querySelector('button'); const oldText = btn.innerText; btn.innerText = '⏳'; btn.disabled = true; try { const fd = new FormData(formAddRule); fd.append('action', 'add_rule'); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); formAddRule.reset(); await loadBudgetSettingsData(); if (typeof showToast === 'function') showToast("Règle ajoutée", 'success'); } catch (err) { if (typeof showToast === 'function') showToast(err.message, 'error'); else alert("Erreur : " + err.message); } finally { btn.innerText = oldText; btn.disabled = false; } });
|
||||||
|
|
||||||
|
function inlineEditSalary(btnEl) {
|
||||||
|
const id = btnEl.dataset.id; const person = btnEl.dataset.person; const salary = btnEl.dataset.salary; const mensualite = btnEl.dataset.mensualite; const currency = '<?= CURRENCY ?>';
|
||||||
|
document.getElementById(`salary-item-${id}`).innerHTML = `
|
||||||
|
<form onsubmit="submitInlineSalary(event, ${id})" class="bs-form-inline align-center">
|
||||||
|
<div class="bs-input-flex"><strong>${person}</strong></div>
|
||||||
|
<div style="display:flex; align-items:center; gap:4px;"><small class="text-muted">${tr('bs_salary_net')}:</small><input type="number" step="0.01" name="salary" class="pf-input bs-input-fixed" value="${salary}" required> ${currency}</div>
|
||||||
|
<div style="display:flex; align-items:center; gap:4px;"><small class="text-muted">${tr('bs_salary_fees')}:</small><input type="number" step="0.01" name="mensualite" class="pf-input bs-input-fixed" value="${mensualite}" required> ${currency}</div>
|
||||||
|
<button type="submit" class="btn btn-secondary">💾</button>
|
||||||
|
<button type="button" class="btn btn-ghost" onclick="loadBudgetSettingsData()">❌</button>
|
||||||
|
</form>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitInlineSalary(event, id) { event.preventDefault(); const form = event.target; const btn = form.querySelector('button'); btn.innerText = '⏳'; btn.disabled = true; try { const fd = new FormData(form); fd.append('action', 'save_salary'); fd.append('id', id); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); await loadBudgetSettingsData(); if (typeof showToast === 'function') showToast("Salaire mis à jour", 'success'); } catch (err) { if (typeof showToast === 'function') showToast(err.message, 'error'); else alert("Erreur : " + err.message); btn.innerText = '💾'; btn.disabled = false; } }
|
||||||
|
|
||||||
|
async function updateBudgetCurrency(selectEl) { try { const fd = new FormData(); fd.append('action', 'save_currency'); fd.append('currency', selectEl.value); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); if (typeof showToast === 'function') { showToast(`${tr('bs_currency_updated')} ✅`, 'success'); } await loadBudgetSettingsData(); } catch (err) { if (typeof showToast === 'function') showToast(err.message, 'error'); else alert("Erreur : " + err.message); } }
|
||||||
|
|
||||||
|
// CSV
|
||||||
|
function toggleCsvAmountCols(type) { if (type === 'split') { document.getElementById('wrapper_col_credit').style.display = 'block'; document.getElementById('lbl_col_debit').innerText = tr('bs_csv_col_debit'); } else { document.getElementById('wrapper_col_credit').style.display = 'none'; document.getElementById('lbl_col_debit').innerText = tr('bs_csv_col_amount'); } }
|
||||||
|
function renderCsvMapping(mapping) { if (!mapping) return; document.getElementById('csv_delimiter').value = mapping.delimiter || ';'; document.getElementById('csv_date_format').value = mapping.date_format || 'd/m/Y'; document.getElementById('csv_col_date').value = mapping.col_date ?? 0; document.getElementById('csv_col_label').value = mapping.col_label ?? 1; document.getElementById('csv_col_ref').value = mapping.col_ref ?? 3; const amountType = mapping.amount_type || 'split'; document.getElementById('csv_amount_type').value = amountType; document.getElementById('csv_col_debit').value = mapping.col_debit ?? 8; document.getElementById('csv_col_credit').value = mapping.col_credit ?? 9; toggleCsvAmountCols(amountType); }
|
||||||
|
|
||||||
|
async function saveCsvMapping(e) { e.preventDefault(); const form = e.target; const btn = form.querySelector('button'); const oldText = btn.innerText; btn.innerText = '⏳...'; btn.disabled = true; try { const fd = new FormData(form); fd.append('action', 'save_csv_mapping'); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); if (typeof showToast === 'function') showToast(tr('bs_csv_saved'), 'success'); else alert(tr('bs_csv_saved')); } catch (err) { if (typeof showToast === 'function') showToast(tr('error_occured') + " : " + err.message, 'error'); else alert(tr('error_occured') + " : " + err.message); } finally { btn.innerText = oldText; btn.disabled = false; } }
|
||||||
|
|
||||||
|
const dropZone = document.getElementById('csv-drop-zone'); const fileInput = document.getElementById('csv_file_input');
|
||||||
|
if (dropZone && fileInput) {
|
||||||
|
dropZone.addEventListener('click', () => fileInput.click());
|
||||||
|
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.style.backgroundColor = 'rgba(0, 123, 255, 0.05)'; dropZone.style.borderColor = 'var(--primary-color, #007bff)'; });
|
||||||
|
dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.style.backgroundColor = ''; dropZone.style.borderColor = 'var(--border-light, #ccc)'; });
|
||||||
|
dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.style.backgroundColor = ''; dropZone.style.borderColor = 'var(--border-light, #ccc)'; if (e.dataTransfer.files.length) { fileInput.files = e.dataTransfer.files; handleCsvUpload(e.dataTransfer.files[0]); } });
|
||||||
|
fileInput.addEventListener('change', function() { if (this.files.length) { handleCsvUpload(this.files[0]); } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCsvUpload(file) {
|
||||||
|
if (!file.name.endsWith('.csv')) {
|
||||||
|
if (typeof showToast === 'function') showToast(tr('bs_csv_err_type'), 'error'); else alert(tr('bs_csv_err_type'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof showToast === 'function') { showToast(`${tr('bs_csv_file_ok')} : ${file.name}`, 'success'); }
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
const text = e.target.result; const delimiter = document.getElementById('csv_delimiter').value || ';';
|
||||||
|
const lines = text.split(/\r?\n/).filter(line => line.trim().length > 0);
|
||||||
|
if (lines.length > 0) {
|
||||||
|
const headers = lines[0].split(delimiter).map(h => h.replace(/^"|"$/g, '').trim());
|
||||||
|
const sampleRows = [];
|
||||||
|
for (let i = 1; i < Math.min(lines.length, 3); i++) { sampleRows.push(lines[i].split(delimiter).map(d => d.replace(/^"|"$/g, '').trim())); }
|
||||||
|
renderCsvTablePreview(headers, sampleRows);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file, 'ISO-8859-1');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveDynamicEstimates(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = e.target;
|
||||||
|
const btn = form.querySelector('button');
|
||||||
|
const oldText = btn.innerText;
|
||||||
|
btn.innerText = '⏳...';
|
||||||
|
btn.disabled = true;
|
||||||
|
try {
|
||||||
|
const fd = new FormData(form);
|
||||||
|
fd.append('action', 'save_dynamic_estimates');
|
||||||
|
await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd });
|
||||||
|
if (typeof showToast === 'function') showToast(window.I18N['fc_settings_updated'] || 'Paramètres enregistrés', 'success');
|
||||||
|
} catch (err) {
|
||||||
|
if (typeof showToast === 'function') showToast((window.I18N['error_occured'] || 'Erreur') + " : " + err.message, 'error');
|
||||||
|
} finally {
|
||||||
|
btn.innerText = oldText;
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
+425
-255
@@ -57,19 +57,29 @@ if (isset($_POST['action']) && $_POST['action'] === 'reopen_month') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
|
if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
|
||||||
$count = 0;
|
$count = 0;
|
||||||
$stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
$stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id, salary_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
$stmtRule = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category) VALUES (?, ?) ON DUPLICATE KEY UPDATE category = VALUES(category)");
|
$stmtRule = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category, budget_item_id) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE category = VALUES(category), budget_item_id = VALUES(budget_item_id)");
|
||||||
|
|
||||||
if (isset($_POST['lines']) && is_array($_POST['lines'])) {
|
if (isset($_POST['lines']) && is_array($_POST['lines'])) {
|
||||||
foreach ($_POST['lines'] as $line) {
|
foreach ($_POST['lines'] as $line) {
|
||||||
if (isset($line['import_check'])) {
|
if (isset($line['import_check'])) {
|
||||||
$cat = $line['cat'];
|
$cat = $line['cat'];
|
||||||
$is_credit = isset($line['is_credit']) ? (int)$line['is_credit'] : 0;
|
$is_credit = isset($line['is_credit']) ? (int)$line['is_credit'] : 0;
|
||||||
$budgetItemId = !empty($line['budget_item_id']) ? (int)$line['budget_item_id'] : null;
|
|
||||||
|
// 🔥 NOUVEAU TRAITEMENT DU BUDGET_ITEM_ID
|
||||||
|
$rawItemId = $line['budget_item_id'] ?? '';
|
||||||
|
$budgetItemId = null;
|
||||||
|
$salaryId = null;
|
||||||
|
if (!empty($rawItemId)) {
|
||||||
|
if (strpos($rawItemId, 'SAL_') === 0) {
|
||||||
|
$salaryId = (int)str_replace('SAL_', '', $rawItemId);
|
||||||
|
} else {
|
||||||
|
$budgetItemId = (int)$rawItemId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$holidayId = !empty($line['holiday_id']) ? (int)$line['holiday_id'] : null;
|
$holidayId = !empty($line['holiday_id']) ? (int)$line['holiday_id'] : null;
|
||||||
$gestionMonthLine = !empty($line['gestion_month']) ? $line['gestion_month'] . '-01' : $viewMonthDate;
|
$gestionMonthLine = !empty($line['gestion_month']) ? $line['gestion_month'] . '-01' : $viewMonthDate;
|
||||||
|
|
||||||
@@ -80,8 +90,8 @@ if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
|
|||||||
$dateToSave = $line['date'];
|
$dateToSave = $line['date'];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$stmtExp->execute([$dateToSave, $gestionMonthLine, $cat, $line['label'], $finalAmount, $line['ref'], $budgetItemId, $holidayId]);
|
$stmtExp->execute([$dateToSave, $gestionMonthLine, $cat, $line['label'], $finalAmount, $line['ref'], $budgetItemId, $holidayId, $salaryId]);
|
||||||
$stmtRule->execute([$line['label'], $cat]);
|
$stmtRule->execute([$line['label'], $cat, $budgetItemId]);
|
||||||
$count++;
|
$count++;
|
||||||
} catch (Exception $e) { continue; }
|
} catch (Exception $e) { continue; }
|
||||||
}
|
}
|
||||||
@@ -110,7 +120,15 @@ $showPreview = false;
|
|||||||
if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
|
if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
|
||||||
$file = $_FILES['csv_file']['tmp_name'];
|
$file = $_FILES['csv_file']['tmp_name'];
|
||||||
$handle = fopen($file, "r");
|
$handle = fopen($file, "r");
|
||||||
$rules = []; try { $rules = $pdo->query("SELECT keyword, category FROM pf_import_rules")->fetchAll(PDO::FETCH_KEY_PAIR); } catch(Exception $e){}
|
|
||||||
|
$rules = [];
|
||||||
|
try {
|
||||||
|
$stmtRules = $pdo->query("SELECT keyword, category, budget_item_id FROM pf_import_rules");
|
||||||
|
while ($r = $stmtRules->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
$rules[$r['keyword']] = ['cat' => $r['category'], 'budget_item_id' => $r['budget_item_id']];
|
||||||
|
}
|
||||||
|
} catch(Exception $e){}
|
||||||
|
|
||||||
$existingRefs = []; try { $existingRefs = $pdo->query("SELECT import_ref FROM pf_expenses WHERE import_ref IS NOT NULL")->fetchAll(PDO::FETCH_COLUMN); } catch(Exception $e){}
|
$existingRefs = []; try { $existingRefs = $pdo->query("SELECT import_ref FROM pf_expenses WHERE import_ref IS NOT NULL")->fetchAll(PDO::FETCH_COLUMN); } catch(Exception $e){}
|
||||||
fgetcsv($handle, 1000, ";", "\"", "\\");
|
fgetcsv($handle, 1000, ";", "\"", "\\");
|
||||||
|
|
||||||
@@ -129,18 +147,59 @@ if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
|
|||||||
$isDuplicate = in_array($uniqueKey, $existingRefs);
|
$isDuplicate = in_array($uniqueKey, $existingRefs);
|
||||||
|
|
||||||
$suggestedCat = '';
|
$suggestedCat = '';
|
||||||
foreach ($rules as $kw => $c) { if (stripos($label, $kw) !== false) { $suggestedCat = $c; break; } }
|
$suggestedItemId = null;
|
||||||
|
foreach ($rules as $kw => $ruleData) {
|
||||||
|
if (stripos($label, $kw) !== false) {
|
||||||
|
$suggestedCat = $ruleData['cat'];
|
||||||
|
$suggestedItemId = $ruleData['budget_item_id'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$csvData[] = ['date'=>$dateSql, 'label'=>$label, 'amount'=>$amount, 'cat'=>$suggestedCat, 'ref'=>$uniqueKey, 'is_duplicate'=>$isDuplicate, 'is_credit'=>$isCredit];
|
$csvData[] = ['date'=>$dateSql, 'label'=>$label, 'amount'=>$amount, 'cat'=>$suggestedCat, 'suggested_item_id'=>$suggestedItemId, 'ref'=>$uniqueKey, 'is_duplicate'=>$isDuplicate, 'is_credit'=>$isCredit];
|
||||||
}
|
}
|
||||||
fclose($handle);
|
fclose($handle);
|
||||||
$showPreview = true;
|
$showPreview = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 3. RECUPERATION DES DONNEES ET CALCULS
|
// 3. RECUPERATION DES DONNEES ET CALCULS (100% DYNAMIQUE)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
// --- LECTURE DYNAMIQUE DES CATEGORIES ---
|
||||||
|
$stmtCats = $pdo->query("SELECT * FROM pf_budget_categories ORDER BY type DESC, sort_order ASC, label ASC");
|
||||||
|
$dbCategories = $stmtCats->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$categoriesConfig = [];
|
||||||
|
foreach ($dbCategories as $c) {
|
||||||
|
$catType = ($c['type'] === 'Income') ? 'credit' : 'debit';
|
||||||
|
$categoriesConfig[$c['code']] = [
|
||||||
|
'type' => $catType,
|
||||||
|
'db_type' => $c['type'],
|
||||||
|
'label' => ($c['icon'] ? $c['icon'] . ' ' : '') . $c['label'],
|
||||||
|
'budget' => 0, // Sera rempli par les règles
|
||||||
|
'color' => $c['color'] ?: '#64748b',
|
||||||
|
'suggestions' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback "Autres" au cas où la BDD serait vide ou pour les dépenses non classées
|
||||||
|
if (!isset($categoriesConfig['AUTRES'])) {
|
||||||
|
$categoriesConfig['AUTRES'] = [
|
||||||
|
'type'=>'debit', 'db_type'=>'Expense', 'label'=>'📁 Autres / Divers',
|
||||||
|
'budget'=>0, 'color'=>'#94a3b8', 'suggestions'=>[]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peuplement dynamique des suggestions via les règles existantes
|
||||||
|
$stmtRules = $pdo->query("SELECT keyword, category FROM pf_import_rules");
|
||||||
|
while ($rule = $stmtRules->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
if (isset($categoriesConfig[$rule['category']])) {
|
||||||
|
$categoriesConfig[$rule['category']]['suggestions'][] = $rule['keyword'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$stmtCheckClose = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'month_closure' AND reference_id = ?");
|
$stmtCheckClose = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'month_closure' AND reference_id = ?");
|
||||||
$stmtCheckClose->execute([$viewMonthDate]);
|
$stmtCheckClose->execute([$viewMonthDate]);
|
||||||
$closureJson = $stmtCheckClose->fetchColumn();
|
$closureJson = $stmtCheckClose->fetchColumn();
|
||||||
@@ -170,27 +229,44 @@ $allExpenses = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
$paidItemIds = array_column(array_filter($allExpenses, fn($e) => !empty($e['budget_item_id'])), 'budget_item_id');
|
$paidItemIds = array_column(array_filter($allExpenses, fn($e) => !empty($e['budget_item_id'])), 'budget_item_id');
|
||||||
$realExpensesLabels = array_column(array_filter($allExpenses, fn($e) => $e['amount'] < 0), 'label');
|
$realExpensesLabels = array_column(array_filter($allExpenses, fn($e) => $e['amount'] < 0), 'label');
|
||||||
|
|
||||||
$budget_fmcg = 0; $budget_school = 0; $budget_essence = 0; $budget_frais = 0; $budget_income_prevu = 0;
|
$budget_income_prevu = 0;
|
||||||
$total_income = 0; $total_expenses_prevues = 0;
|
$total_income = 0; $total_expenses_prevues = 0;
|
||||||
$reste_a_venir_calc = 0;
|
$reste_a_venir_calc = 0;
|
||||||
$fixedChargesList = []; $incomeList = []; $pending_charges = [];
|
$fixedChargesList = []; $incomeList = []; $pending_charges = [];
|
||||||
|
|
||||||
$stmt = $pdo->query("SELECT id, name, amount, type, category, is_estimate, payment_day, mapping_keywords FROM pf_budget_items ORDER BY name ASC");
|
// ============================================================================
|
||||||
|
// MAPPING DYNAMIQUE DES BUDGETS PRÉVISIONNELS (NOUVELLE LOGIQUE)
|
||||||
|
// ============================================================================
|
||||||
|
// On exclut "SAVINGS" pour s'aligner à 100% avec le recap.php
|
||||||
|
$stmt = $pdo->query("SELECT id, name, amount, type, category, is_estimate, payment_day, mapping_keywords FROM pf_budget_items WHERE category != 'SAVINGS' ORDER BY name ASC");
|
||||||
|
$estimatesList = []; // On prépare la liste pour retenir nos estimations multi-catégories
|
||||||
|
|
||||||
while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||||
$absAmount = abs((float)$item['amount']);
|
$absAmount = abs((float)$item['amount']);
|
||||||
$amt = ($item['type'] === 'Annuel') ? $absAmount / 12 : $absAmount;
|
$amt = ($item['type'] === 'Annuel') ? $absAmount / 12 : $absAmount;
|
||||||
$name = trim($item['name']);
|
$name = trim($item['name']);
|
||||||
|
$catCode = $item['category'];
|
||||||
|
|
||||||
if ($item['category'] === 'expense' && $item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
|
$isIncome = (strtoupper($catCode) === 'INCOME');
|
||||||
$fixedChargesList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
|
|
||||||
}
|
if ($isIncome) {
|
||||||
if ($item['category'] === 'income') {
|
|
||||||
$incomeList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
|
$incomeList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
|
||||||
$total_income += $amt;
|
$total_income += $amt;
|
||||||
$budget_income_prevu += $amt;
|
$budget_income_prevu += $amt;
|
||||||
|
|
||||||
|
if (!empty($catCode) && isset($categoriesConfig[$catCode])) {
|
||||||
|
$categoriesConfig[$catCode]['budget'] += $amt;
|
||||||
|
} else {
|
||||||
|
$incomeCatKey = array_key_first(array_filter($categoriesConfig, fn($c) => $c['db_type'] === 'Income'));
|
||||||
|
if ($incomeCatKey) $categoriesConfig[$incomeCatKey]['budget'] += $amt;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$total_expenses_prevues += $amt;
|
$total_expenses_prevues += $amt;
|
||||||
if ($item['category'] === 'expense' && $item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
|
|
||||||
|
// 1. C'est une charge fixe (Non-variable)
|
||||||
|
if ($item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
|
||||||
|
$fixedChargesList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
|
||||||
|
|
||||||
$isPaid = false;
|
$isPaid = false;
|
||||||
if (in_array($item['id'], $paidItemIds)) $isPaid = true;
|
if (in_array($item['id'], $paidItemIds)) $isPaid = true;
|
||||||
elseif (!empty($item['mapping_keywords'])) {
|
elseif (!empty($item['mapping_keywords'])) {
|
||||||
@@ -204,83 +280,133 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
|||||||
}
|
}
|
||||||
if (!$isPaid) {
|
if (!$isPaid) {
|
||||||
$reste_a_venir_calc += $absAmount;
|
$reste_a_venir_calc += $absAmount;
|
||||||
$pending_charges[] = ['name' => $name, 'amount' => $absAmount];
|
// Ajouté dans le tableau avec un tooltip vide
|
||||||
|
$pending_charges[] = ['name' => $name, 'amount' => $absAmount, 'tooltip' => 'Charge fixe en attente'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. C'est une estimation (Variable)
|
||||||
|
if ((int)$item['is_estimate'] === 1) {
|
||||||
|
$calcDetails = "";
|
||||||
|
|
||||||
|
// --- 🚀 INTERCEPTION DYNAMIQUE ---
|
||||||
|
if (isset($item['is_dynamic']) && $item['is_dynamic'] == 1 && !empty($item['dynamic_code'])) {
|
||||||
|
require_once __DIR__ . '/../includes/DynamicBudgetCalculator.php';
|
||||||
|
// Facturation à terme échu : on recule d'un mois
|
||||||
|
$prevDate = date('Y-m-d', strtotime('-1 month', strtotime("$viewY-$viewM-01")));
|
||||||
|
$calcYear = date('Y', strtotime($prevDate));
|
||||||
|
$calcMonth = date('m', strtotime($prevDate));
|
||||||
|
|
||||||
|
$calc = new DynamicBudgetCalculator($pdo, $calcYear, $calcMonth);
|
||||||
|
$estimateData = $calc->getEstimate($item['dynamic_code']);
|
||||||
|
$amt = $estimateData['amount'];
|
||||||
|
$calcDetails = $estimateData['details'];
|
||||||
|
}
|
||||||
|
// --------------------------------
|
||||||
|
|
||||||
|
$estimatesList[] = [
|
||||||
|
'name' => $name,
|
||||||
|
'amount' => $amt,
|
||||||
|
'categories' => !empty($catCode) ? array_map('trim', explode(',', $catCode)) : [],
|
||||||
|
'details' => $calcDetails
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// 3. Attribution du budget aux jauges visuelles de la page
|
||||||
|
// Pour ne pas tout casser, on donne tout le plafond visuel à la 1ère catégorie de la liste
|
||||||
|
if (!empty($catCode)) {
|
||||||
|
$catCodesArray = array_map('trim', explode(',', $catCode));
|
||||||
|
$firstCat = $catCodesArray[0];
|
||||||
|
if (isset($categoriesConfig[$firstCat])) {
|
||||||
|
$categoriesConfig[$firstCat]['budget'] += $amt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($name === 'Estimacio F&B & beauty') $budget_fmcg = $amt;
|
|
||||||
elseif ($name === 'Estimacio escola') $budget_school = $amt;
|
|
||||||
elseif ($name === 'Estimation gasolina') $budget_essence = $amt;
|
|
||||||
elseif ((int)$item['is_estimate'] === 0 && $item['type'] === 'Mensuel' && $item['category'] === 'expense') { $budget_frais += $absAmount; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$budget_autres = max(0, $total_income - $total_expenses_prevues);
|
$stmtSalaries = $pdo->query("SELECT id, person, salary FROM pf_salary_config WHERE year = " . $viewY);
|
||||||
|
while ($sal = $stmtSalaries->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
$incomeList[] = [
|
||||||
|
'id' => 'SAL_' . $sal['id'],
|
||||||
|
'name' => 'Salaire ' . $sal['person'],
|
||||||
|
'amount' => (float)$sal['salary']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// TRADUCTION DES CATÉGORIES
|
// L'enveloppe "Autres" prend tout le reste du budget non alloué
|
||||||
$categoriesConfig = [
|
$categoriesConfig['AUTRES']['budget'] = max(0, $total_income - $total_expenses_prevues);
|
||||||
'Income' => ['type'=>'credit', 'label'=>tr('cat_income'), 'budget'=>$budget_income_prevu, 'color'=>'#10b981', 'suggestions'=>[]],
|
|
||||||
'FMCG' => ['type'=>'debit', 'label'=>tr('cat_fmcg'), 'budget'=>$budget_fmcg, 'color'=>'#3b82f6', 'suggestions'=>['Action', 'Carrefour', 'Lidl']],
|
|
||||||
'Essence' => ['type'=>'debit', 'label'=>tr('cat_fuel'), 'budget'=>$budget_essence, 'color'=>'#f59e0b', 'suggestions'=>['Audi', 'Polo']],
|
|
||||||
'School' => ['type'=>'debit', 'label'=>tr('cat_school'), 'budget'=>$budget_school, 'color'=>'#10b981', 'suggestions'=>[]],
|
|
||||||
'Frais' => ['type'=>'debit', 'label'=>tr('cat_fixed'), 'budget'=>$budget_frais, 'color'=>'#ef4444', 'suggestions'=>[]],
|
|
||||||
'Autres' => ['type'=>'debit', 'label'=>tr('cat_others'), 'budget'=>$budget_autres, 'color'=>'#64748b', 'suggestions'=>['Restaurant', 'Cadeau']],
|
|
||||||
'LivretA' => ['type'=>'debit', 'label'=>tr('cat_savings'),'budget'=>0, 'color'=>'#8b5cf6', 'suggestions'=>['Virement']]
|
|
||||||
];
|
|
||||||
|
|
||||||
$totals = array_fill_keys(array_keys($categoriesConfig), 0);
|
$totals = array_fill_keys(array_keys($categoriesConfig), 0);
|
||||||
$expensesByCategory = array_fill_keys(array_keys($categoriesConfig), []);
|
$expensesByCategory = array_fill_keys(array_keys($categoriesConfig), []);
|
||||||
$total_rentrees = 0;
|
$total_rentrees = 0;
|
||||||
$depenses_reelles = 0;
|
$depenses_reelles = 0;
|
||||||
|
|
||||||
|
// VENTILATION DYNAMIQUE DES DÉPENSES
|
||||||
foreach ($allExpenses as $exp) {
|
foreach ($allExpenses as $exp) {
|
||||||
$cat = $exp['category'];
|
$cat = $exp['category'];
|
||||||
if (!isset($totals[$cat])) $cat = 'Autres';
|
if (!isset($categoriesConfig[$cat])) $cat = 'AUTRES';
|
||||||
|
|
||||||
$val = (float)$exp['amount'];
|
$val = (float)$exp['amount'];
|
||||||
|
|
||||||
if ($cat === 'Income') { $totals[$cat] += $val; }
|
if ($categoriesConfig[$cat]['db_type'] === 'Income') {
|
||||||
else {
|
$totals[$cat] += $val;
|
||||||
if ($val > 0) $categoriesConfig[$cat]['budget'] += $val;
|
} else {
|
||||||
|
if ($val > 0) $categoriesConfig[$cat]['budget'] += $val; // Remboursement
|
||||||
else $totals[$cat] += abs($val);
|
else $totals[$cat] += abs($val);
|
||||||
}
|
}
|
||||||
|
|
||||||
$expensesByCategory[$cat][] = $exp;
|
$expensesByCategory[$cat][] = $exp;
|
||||||
|
|
||||||
if ($val > 0) {
|
if ($val > 0) {
|
||||||
if ($cat === 'Income' || $cat !== 'Frais') $total_rentrees += $val;
|
if ($categoriesConfig[$cat]['db_type'] === 'Income') { $total_rentrees += $val; }
|
||||||
else $depenses_reelles -= $val;
|
else { $depenses_reelles -= $val; }
|
||||||
} else {
|
} else {
|
||||||
$depenses_reelles += abs($val);
|
$depenses_reelles += abs($val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reste à venir dynamiques
|
// CALCUL DES RESTES À VENIR BASÉ SUR LES ESTIMATIONS (MACRO)
|
||||||
$ecole_depense = isset($totals['School']) ? $totals['School'] : 0;
|
foreach ($estimatesList as $est) {
|
||||||
$reste_ecole = max(0, $budget_school - $ecole_depense);
|
$spentForEstimate = 0;
|
||||||
if ($reste_ecole > 0) {
|
$detailsHover = [];
|
||||||
$reste_a_venir_calc += $reste_ecole;
|
|
||||||
$pending_charges[] = ['name' => tr('bud_rem_school'), 'amount' => $reste_ecole];
|
|
||||||
}
|
|
||||||
|
|
||||||
$fmcg_depense = isset($totals['FMCG']) ? $totals['FMCG'] : 0;
|
foreach ($est['categories'] as $cCode) {
|
||||||
$reste_fmcg = max(0, $budget_fmcg - $fmcg_depense);
|
if (!empty($cCode) && isset($totals[$cCode])) {
|
||||||
if ($reste_fmcg > 0) {
|
$spentForEstimate += $totals[$cCode];
|
||||||
$reste_a_venir_calc += $reste_fmcg;
|
if ($totals[$cCode] > 0 && isset($categoriesConfig[$cCode])) {
|
||||||
$pending_charges[] = ['name' => tr('bud_rem_fmcg'), 'amount' => $reste_fmcg];
|
$detailsHover[] = strip_tags($categoriesConfig[$cCode]['label']) . " : " . number_format($totals[$cCode], 0) . "€";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$essence_depense = isset($totals['Essence']) ? $totals['Essence'] : 0;
|
$rem = max(0, $est['amount'] - $spentForEstimate);
|
||||||
$reste_essence = max(0, $budget_essence - $essence_depense);
|
if ($rem > 0) {
|
||||||
if ($reste_essence > 0) {
|
$reste_a_venir_calc += $rem;
|
||||||
$reste_a_venir_calc += $reste_essence;
|
$tooltip = !empty($detailsHover) ? implode(' | ', $detailsHover) : 'Aucune dépense pour le moment';
|
||||||
$pending_charges[] = ['name' => tr('bud_rem_fuel'), 'amount' => $reste_essence];
|
|
||||||
|
// --- INJECTION DU DÉTAIL DYNAMIQUE ---
|
||||||
|
if (!empty($est['details'])) {
|
||||||
|
$tooltip .= "\n\n📊 Détail du calcul :\n" . $est['details'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$pending_charges[] = [
|
||||||
|
'name' => 'Reste ' . $est['name'],
|
||||||
|
'amount' => $rem,
|
||||||
|
'tooltip' => $tooltip
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// F. Calculs des KPIs finaux
|
// F. Calculs des KPIs finaux
|
||||||
$rentrees_salaires_reels = $totals['Income'] ?? 0;
|
$rentrees_salaires_reels = 0;
|
||||||
|
foreach($categoriesConfig as $code => $conf) { if($conf['db_type'] === 'Income') $rentrees_salaires_reels += ($totals[$code] ?? 0); }
|
||||||
|
|
||||||
$rentrees_autres = $total_rentrees - $rentrees_salaires_reels;
|
$rentrees_autres = $total_rentrees - $rentrees_salaires_reels;
|
||||||
$salaires_retenus = max($rentrees_salaires_reels, $budget_income_prevu);
|
$salaires_retenus = max($rentrees_salaires_reels, $budget_income_prevu);
|
||||||
|
|
||||||
$capacite_max_calc = $solde_initial + $salaires_retenus + $rentrees_autres;
|
$capacite_max_calc = $solde_initial + $salaires_retenus + $rentrees_autres;
|
||||||
$solde_theorique_calc = ($solde_initial + $total_rentrees) - $depenses_reelles - $reste_a_venir_calc;
|
|
||||||
|
$revenus_a_venir = max(0, $budget_income_prevu - $rentrees_salaires_reels);
|
||||||
|
$solde_theorique_calc = $snapshot['amount'] + $revenus_a_venir - $reste_a_venir_calc;
|
||||||
|
|
||||||
if ($isClosed) {
|
if ($isClosed) {
|
||||||
$solde_actuel = $monthState['solde_actuel'];
|
$solde_actuel = $monthState['solde_actuel'];
|
||||||
@@ -310,7 +436,6 @@ function getDisplayLogic($spent, $bg, $type) {
|
|||||||
return ['pct' => $pct, 'isOver' => $isOver, 'text' => $text];
|
return ['pct' => $pct, 'isOver' => $isOver, 'text' => $text];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Noms des mois traduits
|
|
||||||
$monthNames = [
|
$monthNames = [
|
||||||
1 => tr('month_01'), 2 => tr('month_02'), 3 => tr('month_03'), 4 => tr('month_04'),
|
1 => tr('month_01'), 2 => tr('month_02'), 3 => tr('month_03'), 4 => tr('month_04'),
|
||||||
5 => tr('month_05'), 6 => tr('month_06'), 7 => tr('month_07'), 8 => tr('month_08'),
|
5 => tr('month_05'), 6 => tr('month_06'), 7 => tr('month_07'), 8 => tr('month_08'),
|
||||||
@@ -321,90 +446,90 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
|||||||
|
|
||||||
<div class="budget-view">
|
<div class="budget-view">
|
||||||
|
|
||||||
<div style="background:white; padding:20px; border-radius:16px; box-shadow:var(--shadow-sm); margin-bottom:24px; border:1px solid #e2e8f0; position:relative;">
|
<div class="bud-summary-card">
|
||||||
|
|
||||||
<?php if ($isClosed): ?>
|
<?php if ($isClosed): ?>
|
||||||
<div style="position:absolute; top:0; left:0; right:0; background:#f1f5f9; padding:5px; text-align:center; font-size:0.8rem; font-weight:bold; color:#64748b; border-radius:16px 16px 0 0; border-bottom:1px solid #e2e8f0;">
|
<div class="bud-closed-banner">
|
||||||
🔒 <?= tr('bud_closed_archived') ?>
|
🔒 <?= tr('bud_closed_archived') ?>
|
||||||
</div>
|
</div>
|
||||||
<div style="height:20px;"></div>
|
<div style="height:20px;"></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px; margin-bottom: 20px;">
|
<div class="bud-header-group">
|
||||||
<div style="display:flex; align-items:center; gap:15px;">
|
<div class="bud-header-title-wrapper">
|
||||||
<h2 style="margin:0; font-size:1.3rem; color:#0f172a; text-transform:capitalize;"><?= tr('bud_budget_of') ?> <?= $monthName ?></h2>
|
<h2 class="bud-header-title"><?= tr('bud_budget_of') ?> <?= $monthName ?></h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="action-toolbar" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">
|
<div class="action-toolbar bud-toolbar">
|
||||||
<?php if (!$isClosed): ?>
|
|
||||||
<button type="button" class="pf-btn btn-secondary" onclick="openSuiviModal('importCsvModal')" style="padding:6px 12px; height:auto; width:auto; font-size:0.85rem;">
|
|
||||||
📂 <?= tr('bud_import_csv') ?? 'Importer CSV' ?>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<form method="POST" onsubmit="return confirm('<?= tr('bud_confirm_close') ?>');">
|
|
||||||
<input type="hidden" name="action" value="close_month">
|
|
||||||
<input type="hidden" name="close_month_date" value="<?= $viewMonthDate ?>">
|
|
||||||
<input type="hidden" name="freeze_solde_actuel" value="<?= $solde_actuel ?>">
|
|
||||||
<input type="hidden" name="freeze_capacite_max" value="<?= $capacite_max ?>">
|
|
||||||
<input type="hidden" name="freeze_solde_theorique" value="<?= $solde_theorique ?>">
|
|
||||||
<input type="hidden" name="freeze_reste_a_venir" value="<?= $reste_a_venir ?>">
|
|
||||||
<button type="submit" class="pf-btn" style="background:linear-gradient(135deg, #10b981, #059669); padding:6px 12px; height:auto; width:auto; font-size:0.85rem;">🔒 <?= tr('bud_close_btn') ?></button>
|
|
||||||
</form>
|
|
||||||
<?php else: ?>
|
|
||||||
<form method="POST" onsubmit="return confirm('<?= tr('bud_confirm_reopen') ?>');">
|
|
||||||
<input type="hidden" name="action" value="reopen_month">
|
|
||||||
<button type="submit" class="pf-btn btn-secondary" style="padding:6px 12px; height:auto; width:auto; font-size:0.85rem;">🔓 <?= tr('bud_reopen_btn') ?></button>
|
|
||||||
</form>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="suivi-nav-group">
|
|
||||||
<a href="<?= $prevLink ?>" class="suivi-btn-nav">◀</a>
|
|
||||||
<a href="<?= $defaultLink ?>" class="suivi-btn-nav"><?= tr('bud_active_month_btn') ?></a>
|
|
||||||
<a href="<?= $nextLink ?>" class="suivi-btn-nav">▶</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap:15px; margin-bottom:25px;">
|
|
||||||
<div style="padding:15px; background:#f8fafc; border-radius:12px; position:relative; border:1px solid #e2e8f0;">
|
|
||||||
<div style="font-size:0.85rem; color:#64748b; margin-bottom:4px;"><?= tr('bud_bank_balance') ?> <?= $isClosed ? "(".tr('bud_frozen').")" : "(".tr('bud_current').")" ?></div>
|
|
||||||
<div style="font-size:1.4rem; font-weight:700; color:#0f172a;">
|
|
||||||
<?= number_format($solde_actuel, 2, ',', ' ') ?> €
|
|
||||||
</div>
|
|
||||||
<?php if (!$isClosed): ?>
|
<?php if (!$isClosed): ?>
|
||||||
<button onclick="openSuiviModal('snapshotModal')" style="position:absolute; top:12px; right:12px; background:white; border:1px solid #cbd5e1; border-radius:6px; padding:4px 8px; cursor:pointer; font-size:0.8rem; color:#475569; transition:0.2s;" onmouseover="this.style.background='#f1f5f9'" onmouseout="this.style.background='white'">✏️ <?= tr('bud_update_btn') ?></button>
|
<button type="button" class="pf-btn btn-secondary pf-btn-small" onclick="openSuiviModal('importCsvModal')">
|
||||||
<?php endif; ?>
|
📂 <?= tr('bud_import_csv') ?? 'Importer CSV' ?>
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
<div style="padding:15px; background:#f8fafc; border-radius:12px; border:1px solid #e2e8f0;">
|
<form method="POST" onsubmit="return confirm('<?= tr('bud_confirm_close') ?>');">
|
||||||
<div style="font-size:0.85rem; color:#64748b; margin-bottom:4px;"><?= tr('bud_theoretical_balance') ?></div>
|
<input type="hidden" name="action" value="close_month">
|
||||||
<div style="font-size:1.4rem; font-weight:700; color:<?= $solde_theorique < 0 ? '#ef4444' : '#334155' ?>;">
|
<input type="hidden" name="close_month_date" value="<?= $viewMonthDate ?>">
|
||||||
<?= number_format($solde_theorique, 2, ',', ' ') ?> €
|
<input type="hidden" name="freeze_solde_actuel" value="<?= $solde_actuel ?>">
|
||||||
</div>
|
<input type="hidden" name="freeze_capacite_max" value="<?= $capacite_max ?>">
|
||||||
</div>
|
<input type="hidden" name="freeze_solde_theorique" value="<?= $solde_theorique ?>">
|
||||||
|
<input type="hidden" name="freeze_reste_a_venir" value="<?= $reste_a_venir ?>">
|
||||||
|
<button type="submit" class="pf-btn bud-btn-lock">🔒 <?= tr('bud_close_btn') ?></button>
|
||||||
|
</form>
|
||||||
|
<?php else: ?>
|
||||||
|
<form method="POST" onsubmit="return confirm('<?= tr('bud_confirm_reopen') ?>');">
|
||||||
|
<input type="hidden" name="action" value="reopen_month">
|
||||||
|
<button type="submit" class="pf-btn btn-secondary pf-btn-small">🔓 <?= tr('bud_reopen_btn') ?></button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div style="padding:15px; background:#fef2f2; border-radius:12px; border:1px solid #fecaca; position: relative;">
|
<div class="suivi-nav-group">
|
||||||
<div style="font-size:0.85rem; color:#991b1b; margin-bottom:4px; display:flex; justify-content:space-between; align-items:center;">
|
<a href="<?= $prevLink ?>" class="suivi-btn-nav">◀</a>
|
||||||
<span><?= tr('bud_upcoming_charges') ?></span>
|
<a href="<?= $defaultLink ?>" class="suivi-btn-nav"><?= tr('bud_active_month_btn') ?></a>
|
||||||
<button onclick="toggleDiv('pendingDetailsList')" style="background:none; border:none; cursor:pointer; font-size:1rem; padding:0; filter: grayscale(1); opacity: 0.7; transition: 0.2s;" onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=0.7">👁️</button>
|
<a href="<?= $nextLink ?>" class="suivi-btn-nav">▶</a>
|
||||||
</div>
|
|
||||||
<div style="font-size:1.4rem; font-weight:700; color:#b91c1c;">
|
|
||||||
- <?= number_format($reste_a_venir, 2, ',', ' ') ?> €
|
|
||||||
</div>
|
|
||||||
<div id="pendingDetailsList" style="display:none; margin-top: 12px; padding-top: 12px; border-top: 1px dashed #fca5a5; font-size: 0.8rem; color: #7f1d1d;">
|
|
||||||
<?php if (empty($pending_charges)): ?>
|
|
||||||
<div style="text-align:center; font-style:italic; opacity:0.8;"><?= tr('bud_all_paid') ?></div>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php foreach($pending_charges as $pc): ?>
|
|
||||||
<div style="display:flex; justify-content:space-between; margin-bottom:6px;">
|
|
||||||
<span><?= htmlspecialchars($pc['name']) ?></span>
|
|
||||||
<strong><?= number_format($pc['amount'], 0, ',', ' ') ?> €</strong>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bud-kpi-grid">
|
||||||
|
<div class="bud-kpi-card">
|
||||||
|
<div class="bud-kpi-label"><?= tr('bud_bank_balance') ?> <?= $isClosed ? "(".tr('bud_frozen').")" : "(".tr('bud_current').")" ?></div>
|
||||||
|
<div class="bud-kpi-value">
|
||||||
|
<?= number_format($solde_actuel, 2, ',', ' ') ?> €
|
||||||
|
</div>
|
||||||
|
<?php if (!$isClosed): ?>
|
||||||
|
<button onclick="openSuiviModal('snapshotModal')" class="bud-kpi-edit-btn">✏️ <?= tr('bud_update_btn') ?></button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bud-kpi-card">
|
||||||
|
<div class="bud-kpi-label"><?= tr('bud_theoretical_balance') ?></div>
|
||||||
|
<div class="bud-kpi-value <?= $solde_theorique < 0 ? 'bud-kpi-value--danger' : '' ?>">
|
||||||
|
<?= number_format($solde_theorique, 2, ',', ' ') ?> €
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bud-kpi-card bud-kpi-card--danger">
|
||||||
|
<div class="bud-kpi-label bud-kpi-label--danger">
|
||||||
|
<span><?= tr('bud_upcoming_charges') ?></span>
|
||||||
|
<button onclick="toggleDiv('pendingDetailsList')" class="bud-pending-toggle">👁️</button>
|
||||||
|
</div>
|
||||||
|
<div class="bud-kpi-value bud-kpi-value--danger">
|
||||||
|
- <?= number_format($reste_a_venir, 2, ',', ' ') ?> €
|
||||||
|
</div>
|
||||||
|
<div id="pendingDetailsList" class="bud-pending-list" style="display:none;">
|
||||||
|
<?php if (empty($pending_charges)): ?>
|
||||||
|
<div style="text-align:center; font-style:italic; opacity:0.8;"><?= tr('bud_all_paid') ?></div>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach($pending_charges as $pc): ?>
|
||||||
|
<div class="bud-pending-item" <?= !empty($pc['tooltip']) ? 'title="'.htmlspecialchars($pc['tooltip']).'" style="cursor:help;"' : '' ?>>
|
||||||
|
<span><?= htmlspecialchars($pc['name']) ?></span>
|
||||||
|
<strong><?= number_format($pc['amount'], 0, ',', ' ') ?> €</strong>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
// --- 1. CALCULS DE L'ÉCHELLE DYNAMIQUE ---
|
// --- 1. CALCULS DE L'ÉCHELLE DYNAMIQUE ---
|
||||||
@@ -430,92 +555,91 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div style="margin-top: 65px; margin-bottom: 75px; position: relative; font-family: inherit;">
|
<div class="bud-scale-wrapper">
|
||||||
|
<div class="bud-scale-track">
|
||||||
|
<?php if ($posZero > 0): ?>
|
||||||
|
<div class="bud-scale-zero" style="width: <?= $posZero ?>%;"></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div style="position: relative; height: 24px; background: #e2e8f0; border-radius: 12px; width: 100%; box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);">
|
<div class="bud-scale-bar" style="left: <?= $leftActuel ?>%; width: <?= $widthActuel ?>%; border-radius: <?= $solde_actuel >= 0 ? '0 12px 12px 0' : '12px' ?>;"></div>
|
||||||
<?php if ($posZero > 0): ?>
|
|
||||||
<div style="position: absolute; left: 0; width: <?= $posZero ?>%; height: 100%; background: #fef2f2; border-radius: 12px 0 0 12px;"></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div style="position: absolute; left: <?= $leftActuel ?>%; width: <?= $widthActuel ?>%; height: 100%; background: repeating-linear-gradient(45deg, #fbbf24, #fbbf24 10px, #f59e0b 10px, #f59e0b 20px); border-radius: <?= $solde_actuel >= 0 ? '0 12px 12px 0' : '12px' ?>; transition: width 0.5s ease-out;"></div>
|
<?php if ($solde_theorique < 0): ?>
|
||||||
|
<div class="bud-scale-bar--danger" style="left: <?= $leftDanger ?>%; width: <?= $widthDanger ?>%;"></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php if ($solde_theorique < 0): ?>
|
<div class="bud-scale-marker bud-scale-marker--zero" style="left: <?= $posZero ?>%;"></div>
|
||||||
<div style="position: absolute; left: <?= $leftDanger ?>%; width: <?= $widthDanger ?>%; height: 100%; background: repeating-linear-gradient(45deg, #ef4444, #ef4444 10px, #dc2626 10px, #dc2626 20px); border-radius: 12px 0 0 12px; opacity: 0.95;"></div>
|
<div class="bud-scale-label bud-scale-label--zero" style="left: <?= $posZero ?>%;">0 €</div>
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="position: absolute; left: <?= $posZero ?>%; top: -5px; height: 35px; width: 2px; background: #94a3b8; z-index: 10;"></div>
|
<div class="bud-scale-marker bud-scale-marker--actual" style="left: <?= $posActuel ?>%;"></div>
|
||||||
<div style="position: absolute; left: <?= $posZero ?>%; top: 32px; transform: translateX(-50%); font-size: 0.85rem; font-weight: 800; color: #64748b;">0 €</div>
|
<div class="bud-scale-label bud-scale-label--actual" style="left: <?= $posActuel ?>%;"><?= tr('bud_bar_actual') ?? 'Actuel' ?></div>
|
||||||
|
|
||||||
<div style="position: absolute; left: <?= $posActuel ?>%; top: -15px; height: 40px; width: 3px; background: #1e293b; z-index: 15;"></div>
|
<?php $colorTheo = $solde_theorique >= 0 ? 'var(--primary)' : 'var(--danger)'; ?>
|
||||||
<div style="position: absolute; left: <?= $posActuel ?>%; top: -35px; transform: translateX(-50%); font-size: 0.8rem; font-weight: bold; color: #1e293b;"><?= tr('bud_bar_actual') ?? 'Actuel' ?></div>
|
<div class="bud-scale-marker bud-scale-marker--theo" style="left: <?= $posTheorique ?>%; background: <?= $colorTheo ?>;"></div>
|
||||||
|
<div class="bud-scale-label bud-scale-label--theo" style="left: <?= $posTheorique ?>%; color: <?= $colorTheo ?>;"><?= tr('bud_bar_theoretical') ?? 'Théorique' ?></div>
|
||||||
|
|
||||||
<div style="position: absolute; left: <?= $posTheorique ?>%; top: 24px; height: 30px; width: 3px; background: <?= $solde_theorique >= 0 ? '#8b5cf6' : '#ef4444' ?>; z-index: 12;"></div>
|
<?php
|
||||||
<div style="position: absolute; left: <?= $posTheorique ?>%; top: 54px; transform: translateX(-50%); font-size: 0.85rem; font-weight: bold; color: <?= $solde_theorique >= 0 ? '#8b5cf6' : '#ef4444' ?>; white-space: nowrap;"><?= tr('bud_bar_theoretical') ?? 'Théorique' ?></div>
|
$overlapMax = abs($posMax - $posActuel) < 35;
|
||||||
|
$topMaxText = $overlapMax ? '-60px' : '-35px';
|
||||||
<?php
|
$heightMaxLine = $overlapMax ? '65px' : '40px';
|
||||||
$overlapMax = abs($posMax - $posActuel) < 35;
|
$topMaxLine = $overlapMax ? '-40px' : '-15px';
|
||||||
$topMaxText = $overlapMax ? '-60px' : '-35px';
|
?>
|
||||||
$heightMaxLine = $overlapMax ? '65px' : '40px';
|
<div class="bud-scale-marker bud-scale-marker--max" style="left: <?= $posMax ?>%; top: <?= $topMaxLine ?>; height: <?= $heightMaxLine ?>;"></div>
|
||||||
$topMaxLine = $overlapMax ? '-40px' : '-15px';
|
|
||||||
?>
|
|
||||||
<div style="position: absolute; left: <?= $posMax ?>%; top: <?= $topMaxLine ?>; height: <?= $heightMaxLine ?>; width: 2px; background: #cbd5e1; z-index: 10;"></div>
|
|
||||||
|
|
||||||
<div style="position: absolute; left: <?= $posMax ?>%; top: <?= $topMaxText ?>; transform: translateX(calc(-100% + 10px)); font-size: 0.8rem; font-weight: bold; color: #64748b; white-space: nowrap; cursor: help;" title="<?= sprintf(tr('bud_capacity_tooltip'), number_format($solde_initial, 0), number_format($salaires_retenus, 0), number_format($rentrees_autres, 0)) ?>">
|
|
||||||
<?= tr('bud_bar_max_cap') ?? 'Capacité Max' ?> : <?= number_format($capacite_max, 0, ',', ' ') ?> € ℹ️
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div class="bud-scale-label bud-scale-label--max" style="left: <?= $posMax ?>%; top: <?= $topMaxText ?>; transform: translateX(calc(-100% + 10px));" title="<?= sprintf(tr('bud_capacity_tooltip'), number_format($solde_initial, 0), number_format($salaires_retenus, 0), number_format($rentrees_autres, 0)) ?>">
|
||||||
|
<?= tr('bud_bar_max_cap') ?? 'Capacité Max' ?> : <?= number_format($capacite_max, 0, ',', ' ') ?> € ℹ️
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="categories-grid" style="display:grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap:24px;">
|
<div class="bud-categories-grid">
|
||||||
<?php foreach ($categoriesConfig as $key => $conf): ?>
|
<?php foreach ($categoriesConfig as $key => $conf): ?>
|
||||||
<div class="cat-card" <?= $isClosed ? 'style="opacity:0.8;"' : '' ?>>
|
<div class="cat-card" <?= $isClosed ? 'style="opacity:0.8;"' : '' ?>>
|
||||||
<div class="cat-card-header" style="background:<?= $conf['color'] ?>15; border-bottom:1px solid <?= $conf['color'] ?>30;">
|
<div class="cat-card-header" style="background:<?= $conf['color'] ?>15; border-bottom:1px solid <?= $conf['color'] ?>30;">
|
||||||
<div class="cat-card-title-group">
|
<div class="cat-card-title-group">
|
||||||
<h3 style="margin:0; font-size:1rem; color:<?= $conf['color'] ?>;"><?= $conf['label'] ?></h3>
|
<h3 style="margin:0; font-size:1rem; color:<?= $conf['color'] ?>;"><?= $conf['label'] ?></h3>
|
||||||
<div class="cat-card-subtitle">
|
<div class="cat-card-subtitle">
|
||||||
<?php $logic = getDisplayLogic($totals[$key], $conf['budget'], $conf['type']); echo $logic['text']; ?>
|
<?php $logic = getDisplayLogic($totals[$key], $conf['budget'], $conf['type']); echo $logic['text']; ?>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<?php if (!$isClosed): ?>
|
|
||||||
<button class="btn-add-item" style="color:<?= $conf['color'] ?>;" onclick="openAddModal('<?= $key ?>', '<?= addslashes($conf['label']) ?>')">+</button>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php $barCol = ($key === 'Income') ? '#10b981' : ($logic['isOver'] ? '#ef4444' : $conf['color']); ?>
|
|
||||||
<div style="background:#f1f5f9; height:4px; width:100%;">
|
|
||||||
<div style="width:<?= $logic['pct'] ?>%; background:<?= $barCol ?>; height:100%;"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="flex:1; max-height:300px; overflow-y:auto; padding:0;">
|
|
||||||
<?php if (empty($expensesByCategory[$key])): ?>
|
|
||||||
<div style="padding:20px; text-align:center; color:#cbd5e1; font-style:italic; font-size:0.85rem;"><?= tr('bud_no_lines') ?></div>
|
|
||||||
<?php else: ?>
|
|
||||||
<table style="width:100%; border-collapse:collapse; font-size:0.85rem;">
|
|
||||||
<?php foreach ($expensesByCategory[$key] as $exp): ?>
|
|
||||||
<tr style="border-bottom:1px solid #f8fafc;">
|
|
||||||
<td style="padding:10px 15px; color:#94a3b8;"><?= date('d/m', strtotime($exp['date_exp'])) ?></td>
|
|
||||||
<td style="padding:10px 5px; font-weight:500;">
|
|
||||||
<?= htmlspecialchars($exp['label']) ?>
|
|
||||||
</td>
|
|
||||||
<td style="padding:10px 15px; text-align:right; font-weight:600; color:<?= $exp['amount'] > 0 ? '#10b981' : '#1e293b' ?>;">
|
|
||||||
<?= $exp['amount'] > 0 ? '+' : '-' ?><?= number_format(abs($exp['amount']), 2) ?>
|
|
||||||
</td>
|
|
||||||
<?php if (!$isClosed): ?>
|
|
||||||
<td style="width:70px; padding-right:10px; text-align:right; white-space:nowrap;">
|
|
||||||
<button class="btn-icon-action edit" onclick='openEditModal(<?= json_encode($exp, JSON_HEX_APOS | JSON_HEX_QUOT) ?>)' title="<?= tr('edit') ?>">✏️</button>
|
|
||||||
<button class="btn-icon-action delete btn-safe-click" onclick="deleteExpense(<?= $exp['id'] ?>)" title="<?= tr('delete') ?>">🗑️</button>
|
|
||||||
</td>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</table>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
</div>
|
||||||
|
<?php if (!$isClosed): ?>
|
||||||
|
<button class="btn-add-item" style="color:<?= $conf['color'] ?>;" onclick="openAddModal('<?= $key ?>', '<?= addslashes(strip_tags($conf['label'])) ?>')">+</button>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
|
||||||
|
<?php $barCol = ($conf['db_type'] === 'Income') ? 'var(--success)' : ($logic['isOver'] ? 'var(--danger)' : $conf['color']); ?>
|
||||||
|
<div style="background:var(--bg-page); height:4px; width:100%;">
|
||||||
|
<div style="width:<?= $logic['pct'] ?>%; background:<?= $barCol ?>; height:100%;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="flex:1; max-height:300px; overflow-y:auto; padding:0;">
|
||||||
|
<?php if (empty($expensesByCategory[$key])): ?>
|
||||||
|
<div style="padding:20px; text-align:center; color:var(--text-muted); font-style:italic; font-size:0.85rem;"><?= tr('bud_no_lines') ?></div>
|
||||||
|
<?php else: ?>
|
||||||
|
<table class="bud-expense-table">
|
||||||
|
<?php foreach ($expensesByCategory[$key] as $exp): ?>
|
||||||
|
<tr class="bud-expense-row">
|
||||||
|
<td class="bud-expense-date"><?= date('d/m', strtotime($exp['date_exp'])) ?></td>
|
||||||
|
<td class="bud-expense-label">
|
||||||
|
<?= htmlspecialchars($exp['label']) ?>
|
||||||
|
</td>
|
||||||
|
<td class="bud-expense-amount" style="color:<?= $exp['amount'] > 0 ? 'var(--success)' : 'var(--text-main)' ?>;">
|
||||||
|
<?= $exp['amount'] > 0 ? '+' : '-' ?><?= number_format(abs($exp['amount']), 2) ?>
|
||||||
|
</td>
|
||||||
|
<?php if (!$isClosed): ?>
|
||||||
|
<td class="bud-expense-actions">
|
||||||
|
<button class="btn-icon-action edit" onclick='openEditModal(<?= json_encode($exp, JSON_HEX_APOS | JSON_HEX_QUOT) ?>)' title="<?= tr('edit') ?>">✏️</button>
|
||||||
|
<button class="btn-icon-action delete btn-safe-click" onclick="deleteExpense(<?= $exp['id'] ?>)" title="<?= tr('delete') ?>">🗑️</button>
|
||||||
|
</td>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</table>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -543,7 +667,7 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
|||||||
<label class="pf-label"><?= tr('bud_category') ?></label>
|
<label class="pf-label"><?= tr('bud_category') ?></label>
|
||||||
<select name="category" id="modalCatSelect" class="pf-input" onchange="handleModalCatChange(this)">
|
<select name="category" id="modalCatSelect" class="pf-input" onchange="handleModalCatChange(this)">
|
||||||
<?php foreach($categoriesConfig as $key => $conf): ?>
|
<?php foreach($categoriesConfig as $key => $conf): ?>
|
||||||
<option value="<?= $key ?>"><?= $conf['label'] ?></option>
|
<option value="<?= $key ?>"><?= strip_tags($conf['label']) ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -562,27 +686,21 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
|||||||
<datalist id="modalSuggestions"></datalist>
|
<datalist id="modalSuggestions"></datalist>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="blockInputSelect" style="margin-bottom:15px; display:none;">
|
|
||||||
<label class="pf-label"><?= tr('bud_beneficiary') ?></label>
|
|
||||||
<select name="label_select" id="schoolSelect" class="pf-input">
|
|
||||||
<option value="Ecole Pol">Ecole Pol</option>
|
|
||||||
<option value="Carole">Carole</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" id="blockInputFrais" style="margin-bottom:15px; display:none;">
|
<div class="form-group" id="blockInputFrais" style="margin-bottom:15px; display:none;">
|
||||||
<label class="pf-label"><?= tr('bud_fixed_charge') ?></label>
|
<label class="pf-label">Lier à une Charge Fixe (Optionnel)</label>
|
||||||
<select name="budget_item_id" id="fraisSelect" class="pf-input" disabled>
|
<select name="budget_item_id" id="fraisSelect" class="pf-input" disabled>
|
||||||
<option value=""><?= tr('bud_select_beneficiary') ?></option>
|
<option value="">-- Aucune --</option>
|
||||||
<?php foreach ($fixedChargesList as $fc): ?><option value="<?= $fc['id'] ?>"><?= htmlspecialchars($fc['name']) ?></option><?php endforeach; ?>
|
<?php foreach ($fixedChargesList as $fc): ?><option value="<?= $fc['id'] ?>"><?= htmlspecialchars($fc['name']) ?></option><?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="blockInputIncome" style="margin-bottom:15px; display:none;">
|
<div class="form-group" id="blockInputIncome" style="margin-bottom:15px; display:none;">
|
||||||
<label class="pf-label"><?= tr('bud_expected_income') ?></label>
|
<label class="pf-label">Lier à un Revenu Prévu (Optionnel)</label>
|
||||||
<select name="budget_item_id" id="incomeSelect" class="pf-input" disabled>
|
<select name="budget_item_id" id="incomeSelect" class="pf-input" disabled>
|
||||||
<option value=""><?= tr('bud_select_beneficiary') ?> </option>
|
<option value="">-- Aucun --</option>
|
||||||
<?php foreach ($incomeList as $inc): ?><option value="<?= $inc['id'] ?>"><?= htmlspecialchars($inc['name']) ?></option><?php endforeach; ?>
|
<?php foreach ($incomeList as $inc): ?>
|
||||||
|
<option value="<?= $inc['id'] ?>"><?= htmlspecialchars($inc['name']) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -690,16 +808,20 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
|||||||
<select name="lines[<?= $idx ?>][cat]" class="pf-input line-select" onchange="handleLineCatChange(this)" <?= $dis ?> style="padding:4px; font-size:0.85rem; flex:1;">
|
<select name="lines[<?= $idx ?>][cat]" class="pf-input line-select" onchange="handleLineCatChange(this)" <?= $dis ?> style="padding:4px; font-size:0.85rem; flex:1;">
|
||||||
<option value="">-- <?= $isCrd ? tr('bud_ignore') : tr('bud_to_define') ?> --</option>
|
<option value="">-- <?= $isCrd ? tr('bud_ignore') : tr('bud_to_define') ?> --</option>
|
||||||
<?php foreach ($categoriesConfig as $k => $c): ?>
|
<?php foreach ($categoriesConfig as $k => $c): ?>
|
||||||
<option value="<?= $k ?>" <?= ($row['cat']===$k)?'selected':'' ?>><?= $c['label'] ?></option>
|
<option value="<?= $k ?>" <?= ($row['cat']===$k)?'selected':'' ?>><?= strip_tags($c['label']) ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
<select name="lines[<?= $idx ?>][budget_item_id]" class="pf-input select-frais" onchange="checkValidation()" style="display:none; padding:4px; font-size:0.85rem; flex:1;" disabled>
|
<select name="lines[<?= $idx ?>][budget_item_id]" class="pf-input select-frais" onchange="checkValidation()" style="display:none; padding:4px; font-size:0.85rem; flex:1;" disabled>
|
||||||
<option value="">-- <?= tr('bud_is_charge') ?> --</option>
|
<option value="">-- Lier à une Charge Fixe --</option>
|
||||||
<?php foreach ($fixedChargesList as $fc): ?><option value="<?= $fc['id'] ?>"><?= htmlspecialchars($fc['name']) ?></option><?php endforeach; ?>
|
<?php foreach ($fixedChargesList as $fc): ?>
|
||||||
|
<option value="<?= $fc['id'] ?>" <?= ($row['suggested_item_id'] == $fc['id']) ? 'selected' : '' ?>><?= htmlspecialchars($fc['name']) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
<select name="lines[<?= $idx ?>][budget_item_id]" class="pf-input select-income" onchange="checkValidation()" style="display:none; padding:4px; font-size:0.85rem; flex:1;" disabled>
|
<select name="lines[<?= $idx ?>][budget_item_id]" class="pf-input select-income" onchange="checkValidation()" style="display:none; padding:4px; font-size:0.85rem; flex:1;" disabled>
|
||||||
<option value="">-- <?= tr('bud_is_income') ?> --</option>
|
<option value="">-- <?= tr('bud_is_income') ?> --</option>
|
||||||
<?php foreach ($incomeList as $inc): ?><option value="<?= $inc['id'] ?>"><?= htmlspecialchars($inc['name']) ?></option><?php endforeach; ?>
|
<?php foreach ($incomeList as $inc): ?>
|
||||||
|
<option value="<?= $inc['id'] ?>" <?= ($row['suggested_item_id'] == $inc['id']) ? 'selected' : '' ?>><?= htmlspecialchars($inc['name']) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -723,7 +845,6 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
|||||||
document.body.classList.add('no-scroll');
|
document.body.classList.add('no-scroll');
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
// --- 1. SÉCURISATION TRADUCTIONS ET LANGUE ---
|
|
||||||
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
||||||
window.I18N = {
|
window.I18N = {
|
||||||
...(window.I18N || {}),
|
...(window.I18N || {}),
|
||||||
@@ -735,34 +856,46 @@ window.I18N = {
|
|||||||
'btn_delete': <?= json_encode(tr('btn_delete')) ?>,
|
'btn_delete': <?= json_encode(tr('btn_delete')) ?>,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- DICTIONNAIRE JS DYNAMIQUE ---
|
||||||
|
const catConfigs = <?= json_encode($categoriesConfig) ?>;
|
||||||
|
|
||||||
const activeViewMonth = '<?= substr($viewMonthDate, 0, 7) ?>';
|
const activeViewMonth = '<?= substr($viewMonthDate, 0, 7) ?>';
|
||||||
function toggleDiv(id) { const el = document.getElementById(id); el.style.display = (el.style.display === 'none') ? 'block' : 'none'; }
|
function toggleDiv(id) { const el = document.getElementById(id); el.style.display = (el.style.display === 'none') ? 'block' : 'none'; }
|
||||||
function openSuiviModal(id) { document.getElementById(id).classList.add('open'); document.body.classList.add('no-scroll'); }
|
function openSuiviModal(id) { document.getElementById(id).classList.add('open'); document.body.classList.add('no-scroll'); }
|
||||||
function closeSuiviModal(id) { document.getElementById(id).classList.remove('open'); document.body.classList.remove('no-scroll');}
|
function closeSuiviModal(id) { document.getElementById(id).classList.remove('open'); document.body.classList.remove('no-scroll');}
|
||||||
|
|
||||||
const suggestions = <?= json_encode(array_map(fn($c) => $c['suggestions'], $categoriesConfig)) ?>;
|
// Gestion dynamique du formulaire modal
|
||||||
|
|
||||||
function handleModalCatChange(select) {
|
function handleModalCatChange(select) {
|
||||||
const catKey = select.value;
|
const catKey = select.value.toUpperCase(); // Sécurité sur la casse
|
||||||
|
const conf = catConfigs[select.value] || {db_type: 'Expense', suggestions: []};
|
||||||
|
|
||||||
document.getElementById('blockInputText').style.display = 'none';
|
document.getElementById('blockInputText').style.display = 'block';
|
||||||
document.getElementById('blockInputSelect').style.display = 'none';
|
|
||||||
document.getElementById('blockInputFrais').style.display = 'none';
|
document.getElementById('blockInputFrais').style.display = 'none';
|
||||||
document.getElementById('blockInputIncome').style.display = 'none';
|
document.getElementById('blockInputIncome').style.display = 'none';
|
||||||
|
|
||||||
document.getElementById('modalLabelInput').required = false;
|
document.getElementById('modalLabelInput').required = true;
|
||||||
document.getElementById('fraisSelect').required = false;
|
document.getElementById('fraisSelect').required = false;
|
||||||
document.getElementById('incomeSelect').required = false;
|
document.getElementById('incomeSelect').required = false;
|
||||||
document.getElementById('fraisSelect').disabled = true;
|
document.getElementById('fraisSelect').disabled = true;
|
||||||
document.getElementById('incomeSelect').disabled = true;
|
document.getElementById('incomeSelect').disabled = true;
|
||||||
|
|
||||||
if (catKey === 'School') { document.getElementById('blockInputSelect').style.display = 'block'; }
|
if (conf.db_type === 'Income') {
|
||||||
else if (catKey === 'Frais') { document.getElementById('blockInputText').style.display = 'block'; document.getElementById('blockInputFrais').style.display = 'block'; document.getElementById('fraisSelect').required = true; document.getElementById('fraisSelect').disabled = false; }
|
const incSel = document.getElementById('incomeSelect');
|
||||||
else if (catKey === 'Income') { document.getElementById('blockInputText').style.display = 'block'; document.getElementById('blockInputIncome').style.display = 'block'; document.getElementById('incomeSelect').required = true; document.getElementById('incomeSelect').disabled = false; }
|
// 👁️ Ne s'affiche que s'il y a des revenus prévus à lier, sinon reste invisible
|
||||||
else {
|
if (incSel.options.length > 1) {
|
||||||
document.getElementById('blockInputText').style.display = 'block'; document.getElementById('modalLabelInput').required = true;
|
document.getElementById('blockInputIncome').style.display = 'block';
|
||||||
const list = document.getElementById('modalSuggestions'); list.innerHTML = '';
|
incSel.disabled = false;
|
||||||
if (suggestions[catKey]) { suggestions[catKey].forEach(i => { const op = document.createElement('option'); op.value = i; list.appendChild(op); }); }
|
}
|
||||||
|
}
|
||||||
|
else if (catKey === 'FIXED') { // 🔒 Strictement pour les Charges Fixes
|
||||||
|
document.getElementById('blockInputFrais').style.display = 'block';
|
||||||
|
document.getElementById('fraisSelect').disabled = false;
|
||||||
|
document.getElementById('fraisSelect').required = true; // 🔥 Devient OBLIGATOIRE
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = document.getElementById('modalSuggestions'); list.innerHTML = '';
|
||||||
|
if (conf.suggestions) {
|
||||||
|
conf.suggestions.forEach(i => { const op = document.createElement('option'); op.value = i; list.appendChild(op); });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -774,8 +907,10 @@ function openAddModal(catKey, catLabel) {
|
|||||||
document.getElementById('modalGestionMonth').value = activeViewMonth;
|
document.getElementById('modalGestionMonth').value = activeViewMonth;
|
||||||
document.getElementById('modalLabelInput').value = "";
|
document.getElementById('modalLabelInput').value = "";
|
||||||
document.getElementById('modalAmount').value = "";
|
document.getElementById('modalAmount').value = "";
|
||||||
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = catKey; handleModalCatChange(catSelect);
|
|
||||||
document.getElementById('modalIsCredit').value = (catKey === 'Income') ? "1" : "0";
|
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = catKey;
|
||||||
|
handleModalCatChange(catSelect);
|
||||||
|
document.getElementById('modalIsCredit').value = (catConfigs[catKey]?.db_type === 'Income') ? "1" : "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEditModal(e) {
|
function openEditModal(e) {
|
||||||
@@ -787,17 +922,35 @@ function openEditModal(e) {
|
|||||||
document.getElementById('modalLabelInput').value = e.label;
|
document.getElementById('modalLabelInput').value = e.label;
|
||||||
document.getElementById('modalIsCredit').value = parseFloat(e.amount) > 0 ? "1" : "0";
|
document.getElementById('modalIsCredit').value = parseFloat(e.amount) > 0 ? "1" : "0";
|
||||||
document.getElementById('modalAmount').value = Math.abs(parseFloat(e.amount));
|
document.getElementById('modalAmount').value = Math.abs(parseFloat(e.amount));
|
||||||
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = e.category; handleModalCatChange(catSelect);
|
|
||||||
if (e.category === 'Frais') document.getElementById('fraisSelect').value = e.budget_item_id;
|
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = e.category;
|
||||||
else if (e.category === 'Income') document.getElementById('incomeSelect').value = e.budget_item_id;
|
handleModalCatChange(catSelect);
|
||||||
|
|
||||||
|
if (catConfigs[e.category]?.db_type === 'Income') document.getElementById('incomeSelect').value = e.budget_item_id;
|
||||||
|
else document.getElementById('fraisSelect').value = e.budget_item_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLineCatChange(select) {
|
// Gestion dynamique du CSV Import
|
||||||
|
function handleLineCatChange(select, isInit = false) {
|
||||||
const row = select.closest('tr');
|
const row = select.closest('tr');
|
||||||
const fSel = row.querySelector('.select-frais'); const iSel = row.querySelector('.select-income');
|
const fSel = row.querySelector('.select-frais');
|
||||||
fSel.style.display = 'none'; iSel.style.display = 'none'; fSel.value = ''; iSel.value = ''; fSel.disabled = true; iSel.disabled = true;
|
const iSel = row.querySelector('.select-income');
|
||||||
if (select.value === 'Frais') { fSel.style.display = 'block'; fSel.disabled = false; }
|
const catKey = select.value.toUpperCase();
|
||||||
else if (select.value === 'Income') { iSel.style.display = 'block'; iSel.disabled = false; }
|
const conf = catConfigs[select.value] || null;
|
||||||
|
|
||||||
|
fSel.style.display = 'none'; iSel.style.display = 'none';
|
||||||
|
if (!isInit) { fSel.value = ''; iSel.value = ''; }
|
||||||
|
fSel.disabled = true; iSel.disabled = true;
|
||||||
|
|
||||||
|
if (conf && conf.db_type === 'Income') {
|
||||||
|
// 👁️ Ne s'affiche que s'il y a des revenus à lier
|
||||||
|
if (iSel.options.length > 1) {
|
||||||
|
iSel.style.display = 'block'; iSel.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (catKey === 'FIXED') { // 🔒 Strictement pour les Charges Fixes
|
||||||
|
fSel.style.display = 'block'; fSel.disabled = false;
|
||||||
|
}
|
||||||
checkValidation();
|
checkValidation();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,27 +959,45 @@ function toggleAll(src) { document.querySelectorAll('.line-checkbox:not([disable
|
|||||||
function checkValidation() {
|
function checkValidation() {
|
||||||
let miss = 0;
|
let miss = 0;
|
||||||
document.querySelectorAll('.line-checkbox:checked').forEach(cb => {
|
document.querySelectorAll('.line-checkbox:checked').forEach(cb => {
|
||||||
const row = cb.closest('tr'); const isCrd = row.querySelector('.is-credit-flag').value === '1'; const cat = row.querySelector('.line-select').value;
|
const row = cb.closest('tr');
|
||||||
let v = true; if (cat==="") { if(!isCrd) v = false; } else if (cat==='Frais' && row.querySelector('.select-frais').value==="") v=false; else if (cat==='Income' && row.querySelector('.select-income').value==="") v=false;
|
const isCrd = row.querySelector('.is-credit-flag').value === '1';
|
||||||
|
const cat = row.querySelector('.line-select').value;
|
||||||
|
const catKey = cat.toUpperCase();
|
||||||
|
const fSel = row.querySelector('.select-frais');
|
||||||
|
|
||||||
|
let v = true;
|
||||||
|
if (cat === "") {
|
||||||
|
// La catégorie est obligatoire pour les dépenses (les revenus sont acceptés sans cat si souhaité)
|
||||||
|
if (!isCrd) v = false;
|
||||||
|
}
|
||||||
|
else if (catKey === 'FIXED' && fSel.value === "") {
|
||||||
|
v = false; // 🔥 Bloque l'import si une Charge Fixe n'est pas liée à une ligne du budget !
|
||||||
|
}
|
||||||
|
|
||||||
if (!v) { miss++; row.style.background = '#fff1f2'; } else row.style.background = '';
|
if (!v) { miss++; row.style.background = '#fff1f2'; } else row.style.background = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
const btn = document.getElementById('btnImport'); const msg = document.getElementById('missingCount');
|
const btn = document.getElementById('btnImport'); const msg = document.getElementById('missingCount');
|
||||||
if(miss>0) { btn.disabled = true; btn.style.opacity=0.5; msg.style.display='inline'; msg.innerText = miss + ' ' + (window.I18N['bud_to_define_js'] || ''); }
|
if(miss > 0) { btn.disabled = true; btn.style.opacity = 0.5; msg.style.display = 'inline'; msg.innerText = miss + ' ' + (window.I18N['bud_to_define_js'] || 'à définir'); }
|
||||||
else { btn.disabled = false; btn.style.opacity=1; msg.style.display='none'; }
|
else { btn.disabled = false; btn.style.opacity = 1; msg.style.display = 'none'; }
|
||||||
}
|
}
|
||||||
|
|
||||||
if(document.getElementById('formMapping')) { document.querySelectorAll('.line-select').forEach(s => handleLineCatChange(s)); checkValidation(); }
|
if (document.getElementById('formMapping')) {
|
||||||
|
document.querySelectorAll('.line-select').forEach(s => handleLineCatChange(s, true));
|
||||||
|
checkValidation();
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('click', (e) => {
|
window.addEventListener('click', (e) => {
|
||||||
if (e.target.classList.contains('pf-modal')) {
|
if (e.target.classList.contains('pf-modal')) {
|
||||||
e.target.style.display = 'none';
|
e.target.classList.remove('open', 'is-active');
|
||||||
|
e.target.style.display = '';
|
||||||
document.body.classList.remove('no-scroll');
|
document.body.classList.remove('no-scroll');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 2. SUPPRESSION ASYNCHRONE ---
|
// --- 2. SUPPRESSION ASYNCHRONE ---
|
||||||
async function deleteExpense(id) {
|
async function deleteExpense(id) {
|
||||||
const confirmed = await pachaConfirm("Suppression", tr('bud_confirm_delete'));
|
const confirmed = await pachaConfirm("Suppression", window.I18N['bud_confirm_delete']);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@@ -856,7 +1027,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
submitBtn.innerText = '⏳ ...';
|
submitBtn.innerText = '⏳ ...';
|
||||||
|
|
||||||
const formData = new FormData(formExpense);
|
const formData = new FormData(formExpense);
|
||||||
// Force l'action ici pour être sûr
|
|
||||||
formData.set('action', 'save_expense_manual');
|
formData.set('action', 'save_expense_manual');
|
||||||
|
|
||||||
const actionUrl = formExpense.getAttribute('action') || 'modules/budget/includes/api/manage-item.php';
|
const actionUrl = formExpense.getAttribute('action') || 'modules/budget/includes/api/manage-item.php';
|
||||||
@@ -871,11 +1041,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
formExpense.reset();
|
formExpense.reset();
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} else {
|
} else {
|
||||||
alert((window.I18N['error_occured'] || 'Erreur') + ' : ' + (result.error || 'Inconnue'));
|
showToast((window.I18N['error_occured'] || 'Erreur') + ' : ' + (result.error || 'Inconnue'), 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur réseau :", error);
|
console.error("Erreur réseau :", error);
|
||||||
alert(window.I18N['bud_err_tech'] || "Erreur critique réseau.");
|
showToast(window.I18N['bud_err_tech'] || "Erreur critique réseau.", 'error');
|
||||||
} finally {
|
} finally {
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
submitBtn.innerText = originalBtnText;
|
submitBtn.innerText = originalBtnText;
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
+2082
-1169
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,208 @@
|
|||||||
|
<?php
|
||||||
|
require __DIR__ . '/../../../../includes/auth.php';
|
||||||
|
require __DIR__ . '/../../../../includes/db.php';
|
||||||
|
require_login();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
$action = $_REQUEST['action'] ?? '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ─── 1. RECUPÉRATION COMPLÈTE DES PARAMÈTRES (INITIALISATION) ───
|
||||||
|
if ($action === 'get_all') {
|
||||||
|
$stmtFoyer = $pdo->query("SELECT zone_scolaire, care_modes FROM pf_foyer_settings LIMIT 1");
|
||||||
|
$foyer = $stmtFoyer->fetch(PDO::FETCH_ASSOC) ?: ['zone_scolaire' => 'C', 'care_modes' => '[]'];
|
||||||
|
|
||||||
|
$stmtPeople = $pdo->query("SELECT id, name, role, care_modes, color FROM pf_people WHERE is_active = 1 ORDER BY role ASC, name ASC");
|
||||||
|
$people = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$stmtLeaves = $pdo->query("SELECT person_id, leave_type, anniversary_date, method, allowance, carry_over_max_days, carry_over_deadline_month FROM pf_person_leave_meta");
|
||||||
|
$leavesRaw = $stmtLeaves->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$leavesMap = [];
|
||||||
|
foreach ($leavesRaw as $leave) {
|
||||||
|
$leavesMap[$leave['person_id']][] = [
|
||||||
|
'type' => $leave['leave_type'],
|
||||||
|
'method' => $leave['method'] ?? 'FIXED',
|
||||||
|
'allowance' => $leave['allowance'] ?? 0,
|
||||||
|
'date' => $leave['anniversary_date'],
|
||||||
|
'carry_over_max_days' => $leave['carry_over_max_days'] ?? 0,
|
||||||
|
'carry_over_deadline_month' => $leave['carry_over_deadline_month'] ?? 12
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmtSettings = $pdo->query("SELECT setting_key, setting_value FROM pf_settings WHERE module = 'calendar'");
|
||||||
|
$calendarSettingsRaw = $stmtSettings->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
|
|
||||||
|
$calendarSettings = [
|
||||||
|
'calendar_default_view' => $calendarSettingsRaw['calendar_default_view'] ?? 'month',
|
||||||
|
'calendar_first_day' => $calendarSettingsRaw['calendar_first_day'] ?? '1',
|
||||||
|
'calendar_working_hours' => $calendarSettingsRaw['calendar_working_hours'] ?? '08:00-19:00'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Catalogue des congés (Nettoyé des règles de report qui sont maintenant sur la personne)
|
||||||
|
$stmtLeaveTypes = $pdo->query("SELECT code, label, default_allowance, reset_month FROM pf_leave_types ORDER BY label ASC");
|
||||||
|
$leaveTypes = $stmtLeaveTypes->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'foyer' => [
|
||||||
|
'zone_scolaire' => $foyer['zone_scolaire'],
|
||||||
|
'care_modes' => json_decode($foyer['care_modes'] ?? '[]', true)
|
||||||
|
],
|
||||||
|
'people' => $people,
|
||||||
|
'leaves' => $leavesMap,
|
||||||
|
'calendar_settings' => $calendarSettings,
|
||||||
|
'leave_types' => $leaveTypes
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 2. SAUVEGARDE DU FOYER (ONGLET 1) ───
|
||||||
|
if ($action === 'save_foyer') {
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
||||||
|
|
||||||
|
$zone = trim($_POST['zone_scolaire'] ?? 'C');
|
||||||
|
$modesRaw = $_POST['care_modes'] ?? '[]';
|
||||||
|
$modesArray = json_encode(array_values(array_unique(array_filter(json_decode($modesRaw, true)))));
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_foyer_settings SET zone_scolaire = ?, care_modes = ? WHERE id = 1");
|
||||||
|
$stmt->execute([$zone, $modesArray]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 3. SAUVEGARDE DES MODES DE GARDE D'UN ENFANT ───
|
||||||
|
if ($action === 'save_child_care_modes') {
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
||||||
|
|
||||||
|
$person_id = (int)($_POST['person_id'] ?? 0);
|
||||||
|
$care_modes = $_POST['care_modes'] ?? '[]';
|
||||||
|
|
||||||
|
if ($person_id > 0) {
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_people SET care_modes = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$care_modes, $person_id]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'ID de l\'enfant manquant.']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 4. GESTION DU CATALOGUE DE CONGÉS (FOYER) ───
|
||||||
|
if ($action === 'get_leave_types') {
|
||||||
|
$stmt = $pdo->query("SELECT * FROM pf_leave_types ORDER BY label ASC");
|
||||||
|
echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'save_leave_type') {
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception(tr('error_occured'));
|
||||||
|
|
||||||
|
$mode = $_POST['mode'] ?? 'add';
|
||||||
|
$code = strtoupper(trim($_POST['code'] ?? ''));
|
||||||
|
$label = trim($_POST['label'] ?? '');
|
||||||
|
$carryMax = (float)($_POST['carry_over_max_days'] ?? 0);
|
||||||
|
$carryDeadline = (int)($_POST['carry_over_deadline_month'] ?? 12);
|
||||||
|
|
||||||
|
if (empty($code) || empty($label)) throw new Exception(tr('fc_err_code_label'));
|
||||||
|
|
||||||
|
if ($mode === 'add') {
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pf_leave_types (code, label, carry_over_max_days, carry_over_deadline_month) VALUES (?, ?, ?, ?)");
|
||||||
|
$stmt->execute([$code, $label, $carryMax, $carryDeadline]);
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_leave_types SET label = ?, carry_over_max_days = ?, carry_over_deadline_month = ? WHERE code = ?");
|
||||||
|
$stmt->execute([$label, $carryMax, $carryDeadline, $code]);
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'delete_leave_type') {
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
||||||
|
|
||||||
|
$code = strtoupper(trim($_POST['code'] ?? ''));
|
||||||
|
if (empty($code)) throw new Exception("Code du congé manquant.");
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
// 1. On supprime d'abord ce congé chez tous les membres à qui on l'avait attribué
|
||||||
|
$stmtMeta = $pdo->prepare("DELETE FROM pf_person_leave_meta WHERE leave_type = ?");
|
||||||
|
$stmtMeta->execute([$code]);
|
||||||
|
|
||||||
|
// 2. Ensuite, on supprime le congé du catalogue global
|
||||||
|
$stmtCat = $pdo->prepare("DELETE FROM pf_leave_types WHERE code = ?");
|
||||||
|
$stmtCat->execute([$code]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 5. GESTION DES CONGÉS INDIVIDUELS (MEMBRES) ───
|
||||||
|
if ($action === 'get_person_leaves') {
|
||||||
|
$personId = (int)($_GET['person_id'] ?? 0);
|
||||||
|
$stmt = $pdo->prepare("SELECT id, leave_type, allowance, method, anniversary_date, carry_over_max_days, carry_over_deadline_month FROM pf_person_leave_meta WHERE person_id = ? ORDER BY leave_type ASC");
|
||||||
|
$stmt->execute([$personId]);
|
||||||
|
echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'add_person_leave') {
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
||||||
|
|
||||||
|
$id = (int)($_POST['meta_id'] ?? 0); // Nouvel ID pour différencier l'Update de l'Insert
|
||||||
|
$personId = (int)($_POST['person_id'] ?? 0);
|
||||||
|
$type = strtoupper(trim($_POST['leave_type'] ?? ''));
|
||||||
|
$allowance = (float)($_POST['allowance'] ?? 0);
|
||||||
|
$resetMonth = (int)($_POST['reset_month'] ?? 1);
|
||||||
|
$carryMax = (float)($_POST['carry_over_max_days'] ?? 0);
|
||||||
|
$carryDeadline = (int)($_POST['carry_over_deadline_month'] ?? 12);
|
||||||
|
|
||||||
|
$method = in_array($_POST['method'] ?? '', ['FIXED', 'ACCUMULATED']) ? $_POST['method'] : 'FIXED';
|
||||||
|
|
||||||
|
if ($personId <= 0 || empty($type)) throw new Exception("Données invalides.");
|
||||||
|
|
||||||
|
// On construit la date anniversaire avec le mois choisi par l'utilisateur
|
||||||
|
$anniversaryDate = "2000-" . str_pad((string)$resetMonth, 2, "0", STR_PAD_LEFT) . "-01";
|
||||||
|
|
||||||
|
if ($id > 0) {
|
||||||
|
// ✏️ UPDATE (Modification)
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_person_leave_meta SET allowance = ?, method = ?, anniversary_date = ?, carry_over_max_days = ?, carry_over_deadline_month = ? WHERE id = ? AND person_id = ?");
|
||||||
|
$stmt->execute([$allowance, $method, $anniversaryDate, $carryMax, $carryDeadline, $id, $personId]);
|
||||||
|
} else {
|
||||||
|
// ➕ INSERT (Ajout)
|
||||||
|
// Vérification des doublons
|
||||||
|
$check = $pdo->prepare("SELECT id FROM pf_person_leave_meta WHERE person_id = ? AND leave_type = ?");
|
||||||
|
$check->execute([$personId, $type]);
|
||||||
|
if ($check->rowCount() > 0) throw new Exception("Ce congé est déjà attribué à cette personne.");
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pf_person_leave_meta (person_id, leave_type, allowance, method, anniversary_date, carry_over_max_days, carry_over_deadline_month) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||||
|
$stmt->execute([$personId, $type, $allowance, $method, $anniversaryDate, $carryMax, $carryDeadline]);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'delete_person_leave') {
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM pf_person_leave_meta WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception("Action inconnue : " . htmlspecialchars($action));
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
@@ -51,3 +51,11 @@
|
|||||||
[2026-05-06T20:38:24+02:00] RAW INPUT: [{"date":"2026-05-08","type":"AVIS","duration":1,"person":"Carole"}]
|
[2026-05-06T20: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: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-06T20:44:36+02:00] RAW INPUT: [{"date":"2026-05-06","type":"PEP_SICK","duration":1,"person":"Carole"}]
|
||||||
|
[2026-05-20T16:40:17+02:00] RAW INPUT: [{"date":"2026-05-19","type":"OFF_CAROLE","duration":1,"person":"Carole"}]
|
||||||
|
[2026-06-16T14:20:35+02:00] RAW INPUT: [{"date":"2026-07-29","type":"CARE_MODE","duration":1,"person":"Centre"}]
|
||||||
|
[2026-06-16T14:20:43+02:00] RAW INPUT: [{"date":"2026-06-16","type":"CARE_MODE","duration":1,"person":"Centre"}]
|
||||||
|
[2026-06-16T14:22:51+02:00] RAW INPUT: [{"date":"2026-06-16","type":"CHILD_SICK","duration":1,"person":"5"}]
|
||||||
|
[2026-06-16T14:23:10+02:00] RAW INPUT: [{"date":"2026-06-16","type":"CHILD_SICK","duration":1,"person":"5"}]
|
||||||
|
[2026-06-16T14:23:33+02:00] RAW INPUT: [{"date":"2026-06-18","type":"CARE_MODE","duration":1,"person":"Nounou"}]
|
||||||
|
[2026-06-16T14:23:54+02:00] RAW INPUT: [{"date":"2026-06-11","type":"HELPER_OFF","duration":1,"person":"1"}]
|
||||||
|
[2026-06-16T14:23:57+02:00] RAW INPUT: [{"date":"2026-06-25","type":"CHILD_SICK","duration":1,"person":"4"}]
|
||||||
@@ -3,13 +3,22 @@ header('Content-Type: application/json');
|
|||||||
require __DIR__ . '/../../../../includes/db.php';
|
require __DIR__ . '/../../../../includes/db.php';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 🔥 LE FIX : On interroge la NOUVELLE table des quotas individuels !
|
||||||
$stmt = $pdo->query("
|
$stmt = $pdo->query("
|
||||||
SELECT person_id, leave_type, initial_balance, balance_year
|
SELECT
|
||||||
FROM pf_leave_balances
|
person_id,
|
||||||
|
leave_type AS type,
|
||||||
|
allowance,
|
||||||
|
method,
|
||||||
|
anniversary_date AS date
|
||||||
|
FROM pf_person_leave_meta
|
||||||
");
|
");
|
||||||
$balances = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$balances = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
echo json_encode(['balances' => $balances]);
|
echo json_encode([
|
||||||
|
'status' => 'success',
|
||||||
|
'balances' => $balances
|
||||||
|
]);
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
@@ -17,3 +26,4 @@ try {
|
|||||||
'message' => $e->getMessage(),
|
'message' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
?>
|
||||||
@@ -4,7 +4,7 @@ require __DIR__ . '/../../../../includes/db.php';
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$stmt = $pdo->query("
|
$stmt = $pdo->query("
|
||||||
SELECT person_id, leave_type, snapshot_date, remaining_balance
|
SELECT person_id, leave_type, snapshot_date, remaining_balance, carry_over_balance
|
||||||
FROM pf_leave_snapshots
|
FROM pf_leave_snapshots
|
||||||
ORDER BY snapshot_date ASC
|
ORDER BY snapshot_date ASC
|
||||||
");
|
");
|
||||||
|
|||||||
@@ -8,77 +8,98 @@ $action = $input['action'] ?? '';
|
|||||||
|
|
||||||
if (!$action) {
|
if (!$action) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Action manquante.']);
|
echo json_encode(['success' => false, 'status' => 'error', 'message' => 'Action manquante.']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// --- SUPPRESSION UNITAIRE ---
|
|
||||||
if ($action === 'delete') {
|
if ($action === 'delete') {
|
||||||
$eventId = (int)($input['event_id'] ?? 0);
|
$eventId = (int)($input['event_id'] ?? 0);
|
||||||
if ($eventId <= 0) {
|
if ($eventId <= 0) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'ID manquant.']);
|
echo json_encode(['success' => false, 'status' => 'error', 'message' => 'ID manquant.']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
$stmt = $pdo->prepare("DELETE FROM pf_events WHERE id = ?");
|
$stmt = $pdo->prepare("DELETE FROM pf_events WHERE id = ?");
|
||||||
$stmt->execute([$eventId]);
|
$stmt->execute([$eventId]);
|
||||||
echo json_encode(['status' => 'success']);
|
echo json_encode(['success' => true, 'status' => 'success']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- MISE À JOUR UNITAIRE ---
|
|
||||||
if ($action === 'update') {
|
if ($action === 'update') {
|
||||||
$eventId = (int)($input['event_id'] ?? 0);
|
$eventId = (int)($input['event_id'] ?? 0);
|
||||||
$newType = $input['new_type'] ?? '';
|
$newType = $input['new_type'] ?? '';
|
||||||
if ($eventId <= 0 || !$newType) {
|
if ($eventId <= 0 || !$newType) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Données manquantes.']);
|
echo json_encode(['success' => false, 'status' => 'error', 'message' => 'Données manquantes.']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
$stmt = $pdo->prepare("UPDATE pf_events SET event_type = ? WHERE id = ?");
|
$stmt = $pdo->prepare("UPDATE pf_events SET event_type = ? WHERE id = ?");
|
||||||
$stmt->execute([$newType, $eventId]);
|
$stmt->execute([$newType, $eventId]);
|
||||||
echo json_encode(['status' => 'success']);
|
echo json_encode(['success' => true, 'status' => 'success']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- SUPPRESSION DE MASSE (Par date et type) ---
|
if ($action === 'bulk_delete_day_types_person') {
|
||||||
// Utilisé quand on ajoute un événement pour nettoyer les doublons potentiels (ex: Off vs Extra)
|
|
||||||
if ($action === 'bulk_delete_day_types') {
|
|
||||||
$dates = $input['dates'] ?? [];
|
$dates = $input['dates'] ?? [];
|
||||||
$types = $input['types'] ?? [];
|
$types = $input['types'] ?? [];
|
||||||
$dates = array_filter($dates, function($d) {
|
$person_id = $input['person_id'] ?? null;
|
||||||
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d);
|
|
||||||
});
|
$dates = array_filter($dates, function($d) { return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); });
|
||||||
|
|
||||||
if (empty($dates) || empty($types)) {
|
if (empty($dates) || empty($types)) {
|
||||||
echo json_encode(['status' => 'success']);
|
echo json_encode(['success' => true, 'status' => 'success']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$datePlaceholders = implode(',', array_fill(0, count($dates), '?'));
|
||||||
|
$typePlaceholders = implode(',', array_fill(0, count($types), '?'));
|
||||||
|
|
||||||
|
$sql = "DELETE FROM pf_events WHERE event_date IN ($datePlaceholders) AND event_type IN ($typePlaceholders)";
|
||||||
|
$params = array_merge($dates, $types);
|
||||||
|
|
||||||
|
// 🔥 LE CORRECTIF : On cherche le 0 en base de données
|
||||||
|
if ($person_id === null || $person_id === '' || (int)$person_id === 0) {
|
||||||
|
$sql .= " AND person_id = 0";
|
||||||
|
} else {
|
||||||
|
$sql .= " AND person_id = ?";
|
||||||
|
$params[] = (int)$person_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'status' => 'success']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'bulk_delete_day_types') {
|
||||||
|
$dates = $input['dates'] ?? [];
|
||||||
|
$types = $input['types'] ?? [];
|
||||||
|
$dates = array_filter($dates, function($d) { return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); });
|
||||||
|
|
||||||
|
if (empty($dates) || empty($types)) {
|
||||||
|
echo json_encode(['success' => true, 'status' => 'success']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Création des placeholders IN (?,?,?)
|
|
||||||
$datePlaceholders = implode(',', array_fill(0, count($dates), '?'));
|
$datePlaceholders = implode(',', array_fill(0, count($dates), '?'));
|
||||||
$typePlaceholders = implode(',', array_fill(0, count($types), '?'));
|
$typePlaceholders = implode(',', array_fill(0, count($types), '?'));
|
||||||
|
|
||||||
$sql = "DELETE FROM pf_events WHERE event_date IN ($datePlaceholders) AND event_type IN ($typePlaceholders)";
|
$sql = "DELETE FROM pf_events WHERE event_date IN ($datePlaceholders) AND event_type IN ($typePlaceholders)";
|
||||||
$stmt = $pdo->prepare($sql);
|
$stmt = $pdo->prepare($sql);
|
||||||
|
|
||||||
// Fusion des tableaux pour l'exécution
|
|
||||||
$stmt->execute(array_merge($dates, $types));
|
$stmt->execute(array_merge($dates, $types));
|
||||||
|
|
||||||
echo json_encode(['status' => 'success']);
|
echo json_encode(['success' => true, 'status' => 'success']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- SUPPRESSION TOTALE SUR DES DATES ---
|
|
||||||
if ($action === 'bulk_delete_all') {
|
if ($action === 'bulk_delete_all') {
|
||||||
$dates = $input['dates'] ?? [];
|
$dates = $input['dates'] ?? [];
|
||||||
$dates = array_filter($dates, function($d) {
|
$dates = array_filter($dates, function($d) { return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); });
|
||||||
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (empty($dates) || empty($types)) {
|
if (empty($dates)) {
|
||||||
echo json_encode(['status' => 'success']);
|
echo json_encode(['success' => true, 'status' => 'success']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,15 +108,15 @@ try {
|
|||||||
$stmt = $pdo->prepare($sql);
|
$stmt = $pdo->prepare($sql);
|
||||||
$stmt->execute($dates);
|
$stmt->execute($dates);
|
||||||
|
|
||||||
echo json_encode(['status' => 'success']);
|
echo json_encode(['success' => true, 'status' => 'success']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Action inconnue
|
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Action non reconnue : ' . $action]);
|
echo json_encode(['success' => false, 'status' => 'error', 'message' => 'Action non reconnue : ' . $action]);
|
||||||
|
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
echo json_encode(['success' => false, 'status' => 'error', 'message' => $e->getMessage()]);
|
||||||
}
|
}
|
||||||
|
?>
|
||||||
@@ -1,33 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
// includes/api/save-events.php
|
// modules/family-calendar/includes/api/save-events.php
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
require __DIR__ . '/../../../../includes/db.php';
|
require_once __DIR__ . '/../../../../includes/db.php';
|
||||||
|
|
||||||
$rawInput = file_get_contents('php://input');
|
|
||||||
$eventsToSave = json_decode($rawInput, true);
|
|
||||||
|
|
||||||
// Optionnel : tu peux commenter/supprimer ces logs en production pour économiser du disque
|
|
||||||
file_put_contents(
|
|
||||||
__DIR__ . '/events-debug.log',
|
|
||||||
"[" . date('c') . "] RAW INPUT: " . $rawInput . PHP_EOL,
|
|
||||||
FILE_APPEND
|
|
||||||
);
|
|
||||||
|
|
||||||
if (empty($eventsToSave) || !is_array($eventsToSave)) {
|
|
||||||
http_response_code(400);
|
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Aucune donnée d\'événement reçue.']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$inserted = [];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. OPTIMISATION : On récupère toutes les personnes d'un coup (Mapping)
|
$rawInput = file_get_contents('php://input');
|
||||||
$stmtPeople = $pdo->query("SELECT id, name FROM pf_people");
|
$eventsToSave = json_decode($rawInput, true);
|
||||||
$peopleMap = [];
|
|
||||||
while ($row = $stmtPeople->fetch(PDO::FETCH_ASSOC)) {
|
if (empty($eventsToSave) || !is_array($eventsToSave)) {
|
||||||
// On crée un tableau associatif : ['Carole' => 1, 'Alex' => 2, etc.]
|
throw new Exception("Aucune donnée d'événement reçue.");
|
||||||
$peopleMap[$row['name']] = $row['id'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
@@ -36,26 +17,36 @@ try {
|
|||||||
VALUES (:event_date, :event_type, :person_id, :duration)";
|
VALUES (:event_date, :event_type, :person_id, :duration)";
|
||||||
$stmt = $pdo->prepare($sql);
|
$stmt = $pdo->prepare($sql);
|
||||||
|
|
||||||
foreach ($eventsToSave as $event) {
|
$inserted = [];
|
||||||
$person_id = null;
|
|
||||||
|
|
||||||
// 2. On vérifie simplement dans notre tableau (plus de requête SQL ici !)
|
foreach ($eventsToSave as $event) {
|
||||||
if (!empty($event['person']) && isset($peopleMap[$event['person']])) {
|
if (empty($event['date']) || empty($event['type'])) {
|
||||||
$person_id = $peopleMap[$event['person']];
|
continue; // Sécurité anti-crash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$person_id = 0;
|
||||||
|
if (!empty($event['person_id']) && is_numeric($event['person_id'])) {
|
||||||
|
$person_id = (int)$event['person_id'];
|
||||||
|
} elseif (!empty($event['person']) && is_numeric($event['person'])) {
|
||||||
|
$person_id = (int)$event['person'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔥 LE FIX : On tronque à 50 caractères pour éviter l'erreur "Data truncated"
|
||||||
|
$safeType = substr(trim($event['type']), 0, 50);
|
||||||
|
$duration = isset($event['duration']) ? (float)$event['duration'] : 1.0;
|
||||||
|
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
':event_date' => $event['date'],
|
':event_date' => $event['date'],
|
||||||
':event_type' => $event['type'],
|
':event_type' => $safeType,
|
||||||
':person_id' => $person_id,
|
':person_id' => $person_id,
|
||||||
':duration' => $event['duration'] ?? 1.0,
|
':duration' => $duration,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$inserted[] = [
|
$inserted[] = [
|
||||||
'id' => $pdo->lastInsertId(),
|
'id' => $pdo->lastInsertId(),
|
||||||
'date' => $event['date'],
|
'date' => $event['date'],
|
||||||
'type' => $event['type'],
|
'type' => $safeType,
|
||||||
'duration' => $event['duration'] ?? 1.0,
|
'duration' => $duration,
|
||||||
'person_id' => $person_id,
|
'person_id' => $person_id,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -63,12 +54,20 @@ try {
|
|||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'inserted' => $inserted,
|
'inserted' => $inserted,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (\Throwable $e) { // 🔥 LE FIX : \Throwable attrape AUSSI les fatals errors PDO
|
||||||
$pdo->rollBack();
|
if (isset($pdo) && $pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Erreur lors de la sauvegarde : ' . $e->getMessage()]);
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Erreur Serveur/SQL : ' . $e->getMessage()
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
?>
|
||||||
@@ -15,13 +15,16 @@ try {
|
|||||||
$stmtDel = $pdo->prepare("DELETE FROM pf_leave_snapshots WHERE person_id = ? AND leave_type = ? AND snapshot_date = ?");
|
$stmtDel = $pdo->prepare("DELETE FROM pf_leave_snapshots WHERE person_id = ? AND leave_type = ? AND snapshot_date = ?");
|
||||||
$stmtDel->execute([$input['person_id'], $input['leave_type'], $input['snapshot_date']]);
|
$stmtDel->execute([$input['person_id'], $input['leave_type'], $input['snapshot_date']]);
|
||||||
|
|
||||||
|
$carry_over_balance = isset($input['carry_over_balance']) ? (float)$input['carry_over_balance'] : 0.00;
|
||||||
|
|
||||||
// Étape 2 : On insère le nouveau solde
|
// Étape 2 : On insère le nouveau solde
|
||||||
$stmt = $pdo->prepare("INSERT INTO pf_leave_snapshots (person_id, leave_type, snapshot_date, remaining_balance) VALUES (?, ?, ?, ?)");
|
$stmt = $pdo->prepare("INSERT INTO pf_leave_snapshots (person_id, leave_type, snapshot_date, remaining_balance, carry_over_balance) VALUES (?, ?, ?, ?, ?)");
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
$input['person_id'],
|
$input['person_id'],
|
||||||
$input['leave_type'],
|
$input['leave_type'],
|
||||||
$input['snapshot_date'],
|
$input['snapshot_date'],
|
||||||
$input['remaining_balance']
|
$input['remaining_balance'],
|
||||||
|
$carry_over_balance
|
||||||
]);
|
]);
|
||||||
|
|
||||||
echo json_encode(['status' => 'success']);
|
echo json_encode(['status' => 'success']);
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
// modules/food/includes/api/api-liste.php
|
||||||
ob_start();
|
ob_start();
|
||||||
require_once dirname(__DIR__, 2) . '/includes/auth.php';
|
require_once dirname(__DIR__, 4) . '/includes/auth.php';
|
||||||
require_login();
|
require_login();
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
@@ -10,11 +11,12 @@ set_exception_handler(function (\Throwable $e) {
|
|||||||
echo json_encode(['ok' => false, 'error' => $e->getMessage()]); exit;
|
echo json_encode(['ok' => false, 'error' => $e->getMessage()]); exit;
|
||||||
});
|
});
|
||||||
|
|
||||||
require_once dirname(__DIR__, 2) . '/includes/db.php';
|
require_once dirname(__DIR__, 4) . '/includes/db.php';
|
||||||
|
|
||||||
// ── Auto-create tables ────────────────────────────────────────────────────────
|
// ── Auto-create tables ────────────────────────────────────────────────────────
|
||||||
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_lists (
|
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_lists (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL DEFAULT 'Ma liste',
|
id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL DEFAULT 'Ma liste',
|
||||||
|
color VARCHAR(20) DEFAULT NULL, list_type VARCHAR(50) DEFAULT NULL,
|
||||||
position INT NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
position INT NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||||
|
|
||||||
@@ -48,6 +50,8 @@ $pdo->exec("CREATE TABLE IF NOT EXISTS pf_item_category_rules (
|
|||||||
try { $pdo->exec("ALTER TABLE pf_grocery_items ADD COLUMN list_id INT NOT NULL DEFAULT 1 AFTER id"); } catch (\Exception $e) {}
|
try { $pdo->exec("ALTER TABLE pf_grocery_items ADD COLUMN list_id INT NOT NULL DEFAULT 1 AFTER id"); } catch (\Exception $e) {}
|
||||||
try { $pdo->exec("ALTER TABLE pf_grocery_items ADD COLUMN category_id INT DEFAULT NULL AFTER list_id"); } catch (\Exception $e) {}
|
try { $pdo->exec("ALTER TABLE pf_grocery_items ADD COLUMN category_id INT DEFAULT NULL AFTER list_id"); } catch (\Exception $e) {}
|
||||||
try { $pdo->exec("ALTER TABLE pf_grocery_items ADD KEY idx_items_list (list_id)"); } catch (\Exception $e) {}
|
try { $pdo->exec("ALTER TABLE pf_grocery_items ADD KEY idx_items_list (list_id)"); } catch (\Exception $e) {}
|
||||||
|
try { $pdo->exec("ALTER TABLE pf_lists ADD COLUMN color VARCHAR(20) DEFAULT NULL AFTER name"); } catch (\Exception $e) {}
|
||||||
|
try { $pdo->exec("ALTER TABLE pf_lists ADD COLUMN list_type VARCHAR(50) DEFAULT NULL AFTER color"); } catch (\Exception $e) {}
|
||||||
|
|
||||||
// ── Seed categories if empty ──────────────────────────────────────────────────
|
// ── Seed categories if empty ──────────────────────────────────────────────────
|
||||||
if ((int)$pdo->query("SELECT COUNT(*) FROM pf_list_categories")->fetchColumn() === 0) {
|
if ((int)$pdo->query("SELECT COUNT(*) FROM pf_list_categories")->fetchColumn() === 0) {
|
||||||
@@ -93,11 +97,9 @@ function liste_ensure_default(PDO $pdo): int {
|
|||||||
return (int)$pdo->query("SELECT id FROM pf_lists ORDER BY position, id LIMIT 1")->fetchColumn();
|
return (int)$pdo->query("SELECT id FROM pf_lists ORDER BY position, id LIMIT 1")->fetchColumn();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Built-in keyword → category position mapping (position maps to seeded order above)
|
|
||||||
function liste_builtin_detect(string $normalized): ?int {
|
function liste_builtin_detect(string $normalized): ?int {
|
||||||
static $map = null;
|
static $map = null;
|
||||||
if ($map === null) $map = [
|
if ($map === null) $map = [
|
||||||
// 1 = Fruits & Légumes
|
|
||||||
'pomme'=>1,'poire'=>1,'banane'=>1,'orange'=>1,'citron'=>1,'raisin'=>1,
|
'pomme'=>1,'poire'=>1,'banane'=>1,'orange'=>1,'citron'=>1,'raisin'=>1,
|
||||||
'fraise'=>1,'framboise'=>1,'myrtille'=>1,'cerise'=>1,'peche'=>1,'abricot'=>1,
|
'fraise'=>1,'framboise'=>1,'myrtille'=>1,'cerise'=>1,'peche'=>1,'abricot'=>1,
|
||||||
'mangue'=>1,'kiwi'=>1,'ananas'=>1,'melon'=>1,'pasteque'=>1,'prune'=>1,
|
'mangue'=>1,'kiwi'=>1,'ananas'=>1,'melon'=>1,'pasteque'=>1,'prune'=>1,
|
||||||
@@ -110,120 +112,96 @@ function liste_builtin_detect(string $normalized): ?int {
|
|||||||
'gingembre'=>1,'avocat'=>1,'endive'=>1,'ciboulette'=>1,'persil'=>1,
|
'gingembre'=>1,'avocat'=>1,'endive'=>1,'ciboulette'=>1,'persil'=>1,
|
||||||
'basilic'=>1,'menthe'=>1,'thym'=>1,'romarin'=>1,'coriandre'=>1,
|
'basilic'=>1,'menthe'=>1,'thym'=>1,'romarin'=>1,'coriandre'=>1,
|
||||||
'girofle'=>1,'citron vert'=>1,'lime'=>1,'pamplemousse'=>1,'litchi'=>1,
|
'girofle'=>1,'citron vert'=>1,'lime'=>1,'pamplemousse'=>1,'litchi'=>1,
|
||||||
// 2 = Viande
|
|
||||||
'boeuf'=>2,'veau'=>2,'poulet'=>2,'porc'=>2,'agneau'=>2,'dinde'=>2,
|
'boeuf'=>2,'veau'=>2,'poulet'=>2,'porc'=>2,'agneau'=>2,'dinde'=>2,
|
||||||
'lapin'=>2,'jambon'=>2,'lardon'=>2,'saucisse'=>2,'saucisson'=>2,
|
'lapin'=>2,'jambon'=>2,'lardon'=>2,'saucisse'=>2,'saucisson'=>2,
|
||||||
'merguez'=>2,'chipolata'=>2,'steak'=>2,'escalope'=>2,'roti'=>2,
|
'merguez'=>2,'chipolata'=>2,'steak'=>2,'escalope'=>2,'roti'=>2,
|
||||||
'gigot'=>2,'filet'=>2,'cuisses'=>2,'viande hachee'=>2,'andouille'=>2,
|
'gigot'=>2,'filet'=>2,'cuisses'=>2,'viande hachee'=>2,'andouille'=>2,
|
||||||
'magret'=>2,'canard'=>2,'foie'=>2,'boudin'=>2,'paupiette'=>2,
|
'magret'=>2,'canard'=>2,'foie'=>2,'boudin'=>2,'paupiette'=>2,
|
||||||
'entrecote'=>2,'cote de boeuf'=>2,'onglet'=>2,'bavette'=>2,
|
'entrecote'=>2,'cote de boeuf'=>2,'onglet'=>2,'bavette'=>2,
|
||||||
// 3 = Poissonnerie
|
|
||||||
'saumon'=>3,'thon'=>3,'dorade'=>3,'bar'=>3,'cabillaud'=>3,'sole'=>3,
|
'saumon'=>3,'thon'=>3,'dorade'=>3,'bar'=>3,'cabillaud'=>3,'sole'=>3,
|
||||||
'truite'=>3,'lieu'=>3,'maquereau'=>3,'sardine'=>3,'anchois'=>3,
|
'truite'=>3,'lieu'=>3,'maquereau'=>3,'sardine'=>3,'anchois'=>3,
|
||||||
'crevette'=>3,'moule'=>3,'huitre'=>3,'coquille'=>3,'poulpe'=>3,
|
'crevette'=>3,'moule'=>3,'huitre'=>3,'coquille'=>3,'poulpe'=>3,
|
||||||
'seiche'=>3,'langoustine'=>3,'lotte'=>3,'merlu'=>3,'colin'=>3,
|
'seiche'=>3,'langoustine'=>3,'lotte'=>3,'merlu'=>3,'colin'=>3,
|
||||||
'tilapia'=>3,'daurade'=>3,'aiglefin'=>3,'homard'=>3,'crabe'=>3,
|
'tilapia'=>3,'daurade'=>3,'aiglefin'=>3,'homard'=>3,'crabe'=>3,
|
||||||
// 4 = Boulangerie
|
|
||||||
'pain'=>4,'baguette'=>4,'brioche'=>4,'croissant'=>4,'pain de mie'=>4,
|
'pain'=>4,'baguette'=>4,'brioche'=>4,'croissant'=>4,'pain de mie'=>4,
|
||||||
'ficelle'=>4,'fougasse'=>4,'ciabatta'=>4,'biscotte'=>4,'naan'=>4,
|
'ficelle'=>4,'fougasse'=>4,'ciabatta'=>4,'biscotte'=>4,'naan'=>4,
|
||||||
'pita'=>4,'toast'=>4,'bagel'=>4,'pain burger'=>4,'wrap'=>4,
|
'pita'=>4,'toast'=>4,'bagel'=>4,'pain burger'=>4,'wrap'=>4,
|
||||||
// 5 = Frais
|
|
||||||
'lait'=>5,'creme'=>5,'yaourt'=>5,'yourt'=>5,'beurre'=>5,
|
'lait'=>5,'creme'=>5,'yaourt'=>5,'yourt'=>5,'beurre'=>5,
|
||||||
'fromage'=>5,'camembert'=>5,'brie'=>5,'comte'=>5,'gruyere'=>5,
|
'fromage'=>5,'camembert'=>5,'brie'=>5,'comte'=>5,'gruyere'=>5,
|
||||||
'emmental'=>5,'mozzarella'=>5,'ricotta'=>5,'mascarpone'=>5,
|
'emmental'=>5,'mozzarella'=>5,'ricotta'=>5,'mascarpone'=>5,
|
||||||
'feta'=>5,'chevre'=>5,'oeuf'=>5,'oeufs'=>5,'creme fraiche'=>5,
|
'feta'=>5,'chevre'=>5,'oeuf'=>5,'oeufs'=>5,'creme fraiche'=>5,
|
||||||
'fromage blanc'=>5,'petit suisse'=>5,'kefir'=>5,
|
'fromage blanc'=>5,'petit suisse'=>5,'kefir'=>5,
|
||||||
// 6 = Surgelés
|
|
||||||
'surgele'=>6,'pizza surgelee'=>6,'frite surgelee'=>6,'poisson pane'=>6,
|
'surgele'=>6,'pizza surgelee'=>6,'frite surgelee'=>6,'poisson pane'=>6,
|
||||||
'legume surgele'=>6,'glace'=>6,'sorbet'=>6,'plat surgele'=>6,
|
'legume surgele'=>6,'glace'=>6,'sorbet'=>6,'plat surgele'=>6,
|
||||||
'nugget'=>6,'edamame'=>6,'petits pois'=>6,'epinards'=>6,
|
'nugget'=>6,'edamame'=>6,'petits pois'=>6,'epinards'=>6,
|
||||||
// 7 = Boissons
|
|
||||||
'eau'=>7,'coca'=>7,'cola'=>7,'limonade'=>7,'sirop'=>7,
|
'eau'=>7,'coca'=>7,'cola'=>7,'limonade'=>7,'sirop'=>7,
|
||||||
'biere'=>7,'vin'=>7,'champagne'=>7,'cafe'=>7,'the'=>7,
|
'biere'=>7,'vin'=>7,'champagne'=>7,'cafe'=>7,'the'=>7,
|
||||||
'tisane'=>7,'soda'=>7,'jus'=>7,'kombucha'=>7,'cidre'=>7,
|
'tisane'=>7,'soda'=>7,'jus'=>7,'kombucha'=>7,'cidre'=>7,
|
||||||
'whisky'=>7,'rhum'=>7,'vodka'=>7,'prosecco'=>7,'rose'=>7,
|
'whisky'=>7,'rhum'=>7,'vodka'=>7,'prosecco'=>7,'rose'=>7,
|
||||||
// 8 = Pâtes, Riz, Féculents
|
|
||||||
'pate'=>8,'spaghetti'=>8,'tagliatelle'=>8,'penne'=>8,'fusilli'=>8,
|
'pate'=>8,'spaghetti'=>8,'tagliatelle'=>8,'penne'=>8,'fusilli'=>8,
|
||||||
'macaroni'=>8,'riz'=>8,'couscous'=>8,'quinoa'=>8,'lentille'=>8,
|
'macaroni'=>8,'riz'=>8,'couscous'=>8,'quinoa'=>8,'lentille'=>8,
|
||||||
'pois chiche'=>8,'farine'=>8,'semoule'=>8,'polenta'=>8,'boulgour'=>8,
|
'pois chiche'=>8,'farine'=>8,'semoule'=>8,'polenta'=>8,'boulgour'=>8,
|
||||||
'orge'=>8,'feculent'=>8,'vermicelle'=>8,'nouille'=>8,'gnocchi'=>8,
|
'orge'=>8,'feculent'=>8,'vermicelle'=>8,'nouille'=>8,'gnocchi'=>8,
|
||||||
// 9 = Épicerie salée
|
|
||||||
'chips'=>9,'crackers'=>9,'olives'=>9,'cornichon'=>9,'capres'=>9,
|
'chips'=>9,'crackers'=>9,'olives'=>9,'cornichon'=>9,'capres'=>9,
|
||||||
'tapenade'=>9,'houmous'=>9,'tzatziki'=>9,'sel'=>9,'poivre'=>9,
|
'tapenade'=>9,'houmous'=>9,'tzatziki'=>9,'sel'=>9,'poivre'=>9,
|
||||||
'epice'=>9,'cube bouillon'=>9,'bouillon'=>9,'noix de cajou'=>9,
|
'epice'=>9,'cube bouillon'=>9,'bouillon'=>9,'noix de cajou'=>9,
|
||||||
'amande'=>9,'pistache'=>9,'noix'=>9,'cacahuete'=>9,
|
'amande'=>9,'pistache'=>9,'noix'=>9,'cacahuete'=>9,
|
||||||
// 10 = Conserves
|
|
||||||
'conserve'=>10,'boite de tomate'=>10,'concentre de tomate'=>10,
|
'conserve'=>10,'boite de tomate'=>10,'concentre de tomate'=>10,
|
||||||
'mais'=>10,'ratatouille'=>10,'cassoulet'=>10,'pate de foie'=>10,
|
'mais'=>10,'ratatouille'=>10,'cassoulet'=>10,'pate de foie'=>10,
|
||||||
'sardine boite'=>10,'thon boite'=>10,'maquereau boite'=>10,
|
'sardine boite'=>10,'thon boite'=>10,'maquereau boite'=>10,
|
||||||
'soupe boite'=>10,'haricot boite'=>10,
|
'soupe boite'=>10,'haricot boite'=>10,
|
||||||
// 11 = Plats cuisinés
|
|
||||||
'quiche'=>11,'lasagne'=>11,'gratin'=>11,'pizza'=>11,'tartiflette'=>11,
|
'quiche'=>11,'lasagne'=>11,'gratin'=>11,'pizza'=>11,'tartiflette'=>11,
|
||||||
'croque'=>11,'hachis parmentier'=>11,'paella'=>11,'moussaka'=>11,
|
'croque'=>11,'hachis parmentier'=>11,'paella'=>11,'moussaka'=>11,
|
||||||
'tarte'=>11,'flamiche'=>11,'pissaladiere'=>11,
|
'tarte'=>11,'flamiche'=>11,'pissaladiere'=>11,
|
||||||
// 12 = Sauces & Condiments
|
|
||||||
'ketchup'=>12,'mayonnaise'=>12,'moutarde'=>12,'vinaigrette'=>12,
|
'ketchup'=>12,'mayonnaise'=>12,'moutarde'=>12,'vinaigrette'=>12,
|
||||||
'sauce soja'=>12,'tabasco'=>12,'huile'=>12,'vinaigre'=>12,
|
'sauce soja'=>12,'tabasco'=>12,'huile'=>12,'vinaigre'=>12,
|
||||||
'sauce tomate'=>12,'pesto'=>12,'sriracha'=>12,'worcester'=>12,
|
'sauce tomate'=>12,'pesto'=>12,'sriracha'=>12,'worcester'=>12,
|
||||||
'nuoc mam'=>12,'sauce'=>12,'condiment'=>12,'worcestershire'=>12,
|
'nuoc mam'=>12,'sauce'=>12,'condiment'=>12,'worcestershire'=>12,
|
||||||
// 13 = Petit déjeuner
|
|
||||||
'cereale'=>13,'muesli'=>13,'granola'=>13,'corn flakes'=>13,
|
'cereale'=>13,'muesli'=>13,'granola'=>13,'corn flakes'=>13,
|
||||||
'miel'=>13,'confiture'=>13,'nutella'=>13,'beurre de cacahuete'=>13,
|
'miel'=>13,'confiture'=>13,'nutella'=>13,'beurre de cacahuete'=>13,
|
||||||
'sirop d erable'=>13,'sirop erable'=>13,'chocolat en poudre'=>13,
|
'sirop d erable'=>13,'sirop erable'=>13,'chocolat en poudre'=>13,
|
||||||
// 14 = Biscuits & Gâteaux
|
|
||||||
'biscuit'=>14,'gateau'=>14,'cookie'=>14,'madeleine'=>14,
|
'biscuit'=>14,'gateau'=>14,'cookie'=>14,'madeleine'=>14,
|
||||||
'financier'=>14,'quatre quarts'=>14,'brownie'=>14,'sable'=>14,
|
'financier'=>14,'quatre quarts'=>14,'brownie'=>14,'sable'=>14,
|
||||||
'speculoos'=>14,'oreo'=>14,'lu'=>14,'petit beurre'=>14,'galette'=>14,
|
'speculoos'=>14,'oreo'=>14,'lu'=>14,'petit beurre'=>14,'galette'=>14,
|
||||||
'palmier'=>14,'macaron'=>14,'eclair'=>14,
|
'palmier'=>14,'macaron'=>14,'eclair'=>14,
|
||||||
// 15 = Confiserie
|
|
||||||
'chocolat'=>15,'bonbon'=>15,'reglisse'=>15,'caramel'=>15,
|
'chocolat'=>15,'bonbon'=>15,'reglisse'=>15,'caramel'=>15,
|
||||||
'nougat'=>15,'marshmallow'=>15,'guimauve'=>15,'sucette'=>15,
|
'nougat'=>15,'marshmallow'=>15,'guimauve'=>15,'sucette'=>15,
|
||||||
'chewing gum'=>15,'pastille'=>15,'calisson'=>15,
|
'chewing gum'=>15,'pastille'=>15,'calisson'=>15,
|
||||||
// 16 = Dessert
|
|
||||||
'yaourt dessert'=>16,'creme dessert'=>16,'mousse au chocolat'=>16,
|
'yaourt dessert'=>16,'creme dessert'=>16,'mousse au chocolat'=>16,
|
||||||
'tiramisu'=>16,'creme brulee'=>16,'flan'=>16,'crepe'=>16,
|
'tiramisu'=>16,'creme brulee'=>16,'flan'=>16,'crepe'=>16,
|
||||||
'madeleine'=>16,'profiterole'=>16,'eclair'=>16,
|
'profiterole'=>16,
|
||||||
// 17 = Beauté & Hygiène
|
|
||||||
'shampoing'=>17,'gel douche'=>17,'savon'=>17,'dentifrice'=>17,
|
'shampoing'=>17,'gel douche'=>17,'savon'=>17,'dentifrice'=>17,
|
||||||
'deodorant'=>17,'crème visage'=>17,'crème corps'=>17,'rasoir'=>17,
|
'deodorant'=>17,'crème visage'=>17,'crème corps'=>17,'rasoir'=>17,
|
||||||
'mousse a raser'=>17,'coton'=>17,'lingette'=>17,'maquillage'=>17,
|
'mousse a raser'=>17,'coton'=>17,'lingette'=>17,'maquillage'=>17,
|
||||||
'parfum'=>17,'brosse a dents'=>17,'fil dentaire'=>17,'serum'=>17,
|
'parfum'=>17,'brosse a dents'=>17,'fil dentaire'=>17,'serum'=>17,
|
||||||
'hydratant'=>17,'demaquillant'=>17,
|
'hydratant'=>17,'demaquillant'=>17,
|
||||||
// 18 = Bébé
|
|
||||||
'couche'=>18,'biberon'=>18,'lait infantile'=>18,'compote bebe'=>18,
|
'couche'=>18,'biberon'=>18,'lait infantile'=>18,'compote bebe'=>18,
|
||||||
'pot bebe'=>18,'puree bebe'=>18,'lingette bebe'=>18,'savon bebe'=>18,
|
'pot bebe'=>18,'puree bebe'=>18,'lingette bebe'=>18,'savon bebe'=>18,
|
||||||
'creme bebe'=>18,'sucette bebe'=>18,
|
'creme bebe'=>18,'sucette bebe'=>18,
|
||||||
// 19 = Entretien
|
|
||||||
'lessive'=>19,'liquide vaisselle'=>19,'nettoyant'=>19,'degraissant'=>19,
|
'lessive'=>19,'liquide vaisselle'=>19,'nettoyant'=>19,'degraissant'=>19,
|
||||||
'deboucheur'=>19,'anticalcaire'=>19,'eponge'=>19,'serpillere'=>19,
|
'deboucheur'=>19,'anticalcaire'=>19,'eponge'=>19,'serpillere'=>19,
|
||||||
'sac poubelle'=>19,'papier toilette'=>19,'sopalin'=>19,'essuie tout'=>19,
|
'sac poubelle'=>19,'papier toilette'=>19,'sopalin'=>19,'essuie tout'=>19,
|
||||||
'aluminium'=>19,'film plastique'=>19,'sac congelation'=>19,
|
'aluminium'=>19,'film plastique'=>19,'sac congelation'=>19,
|
||||||
'nettoyant wc'=>19,'desinfectant'=>19,'vitre'=>19,'javel'=>19,
|
'nettoyant wc'=>19,'desinfectant'=>19,'vitre'=>19,'javel'=>19,
|
||||||
// 20 = Animaux
|
|
||||||
'croquette'=>20,'patee'=>20,'litiere'=>20,'os'=>20,
|
'croquette'=>20,'patee'=>20,'litiere'=>20,'os'=>20,
|
||||||
'friandise animale'=>20,'nourriture chat'=>20,'nourriture chien'=>20,
|
'friandise animale'=>20,'nourriture chat'=>20,'nourriture chien'=>20,
|
||||||
// 21 = Maison & Jardin
|
|
||||||
'ampoule'=>21,'pile'=>21,'bougie'=>21,'allumette'=>21,
|
'ampoule'=>21,'pile'=>21,'bougie'=>21,'allumette'=>21,
|
||||||
'ruban adhesif'=>21,'colle'=>21,'vis'=>21,'terreau'=>21,
|
'ruban adhesif'=>21,'colle'=>21,'vis'=>21,'terreau'=>21,
|
||||||
'engrais'=>21,'pot de fleur'=>21,'arrosoir'=>21,'graine'=>21,
|
'engrais'=>21,'pot de fleur'=>21,'arrosoir'=>21,'graine'=>21,
|
||||||
'tournevis'=>21,'marteau'=>21,'clou'=>21,'cle'=>21,
|
'tournevis'=>21,'marteau'=>21,'clou'=>21,'cle'=>21,
|
||||||
// 22 = Pharmacie
|
|
||||||
'doliprane'=>22,'ibuprofene'=>22,'paracetamol'=>22,'aspirine'=>22,
|
'doliprane'=>22,'ibuprofene'=>22,'paracetamol'=>22,'aspirine'=>22,
|
||||||
'bandage'=>22,'pansement'=>22,'thermometre'=>22,'serum physiologique'=>22,
|
'bandage'=>22,'pansement'=>22,'thermometre'=>22,'serum physiologique'=>22,
|
||||||
'vitamine'=>22,'complement'=>22,'masque'=>22,'gant medical'=>22,
|
'vitamine'=>22,'complement'=>22,'masque'=>22,'gant medical'=>22,
|
||||||
'sirop'=>22,'antihistaminique'=>22,'antidouleur'=>22,
|
'antihistaminique'=>22,'antidouleur'=>22,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Check full phrase
|
|
||||||
if (isset($map[$normalized])) return $map[$normalized];
|
if (isset($map[$normalized])) return $map[$normalized];
|
||||||
|
|
||||||
// Check individual words
|
|
||||||
$words = explode(' ', $normalized);
|
$words = explode(' ', $normalized);
|
||||||
foreach ($words as $w) {
|
foreach ($words as $w) {
|
||||||
if (strlen($w) >= 3 && isset($map[$w])) return $map[$w];
|
if (strlen($w) >= 3 && isset($map[$w])) return $map[$w];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Partial match (contains)
|
|
||||||
foreach ($map as $kw => $catPos) {
|
foreach ($map as $kw => $catPos) {
|
||||||
if (str_contains($normalized, $kw)) return $catPos;
|
if (str_contains($normalized, $kw)) return $catPos;
|
||||||
}
|
}
|
||||||
@@ -234,12 +212,10 @@ function liste_builtin_detect(string $normalized): ?int {
|
|||||||
function liste_detect_category(PDO $pdo, string $label): ?int {
|
function liste_detect_category(PDO $pdo, string $label): ?int {
|
||||||
$norm = liste_normalize_detect($label);
|
$norm = liste_normalize_detect($label);
|
||||||
|
|
||||||
// 1. Learned rules (exact normalized label)
|
|
||||||
$r = $pdo->prepare("SELECT category_id FROM pf_item_category_rules WHERE keyword=? LIMIT 1");
|
$r = $pdo->prepare("SELECT category_id FROM pf_item_category_rules WHERE keyword=? LIMIT 1");
|
||||||
$r->execute([$norm]);
|
$r->execute([$norm]);
|
||||||
if ($row = $r->fetch()) return (int)$row['category_id'];
|
if ($row = $r->fetch()) return (int)$row['category_id'];
|
||||||
|
|
||||||
// 2. Learned rules (individual words)
|
|
||||||
foreach (explode(' ', $norm) as $w) {
|
foreach (explode(' ', $norm) as $w) {
|
||||||
if (strlen($w) < 3) continue;
|
if (strlen($w) < 3) continue;
|
||||||
$r = $pdo->prepare("SELECT category_id FROM pf_item_category_rules WHERE keyword=? LIMIT 1");
|
$r = $pdo->prepare("SELECT category_id FROM pf_item_category_rules WHERE keyword=? LIMIT 1");
|
||||||
@@ -247,11 +223,9 @@ function liste_detect_category(PDO $pdo, string $label): ?int {
|
|||||||
if ($row = $r->fetch()) return (int)$row['category_id'];
|
if ($row = $r->fetch()) return (int)$row['category_id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Built-in dictionary → map position to real category id
|
|
||||||
$pos = liste_builtin_detect($norm);
|
$pos = liste_builtin_detect($norm);
|
||||||
if ($pos === null) return null;
|
if ($pos === null) return null;
|
||||||
|
|
||||||
// Get category id by position order
|
|
||||||
$r = $pdo->query("SELECT id FROM pf_list_categories ORDER BY position, id");
|
$r = $pdo->query("SELECT id FROM pf_list_categories ORDER BY position, id");
|
||||||
$ids = $r->fetchAll(PDO::FETCH_COLUMN);
|
$ids = $r->fetchAll(PDO::FETCH_COLUMN);
|
||||||
return $ids[$pos - 1] ?? null;
|
return $ids[$pos - 1] ?? null;
|
||||||
@@ -263,7 +237,6 @@ function liste_learn(PDO $pdo, string $label, int $category_id): void {
|
|||||||
$pdo->prepare("INSERT INTO pf_item_category_rules (keyword, category_id) VALUES (?,?)
|
$pdo->prepare("INSERT INTO pf_item_category_rules (keyword, category_id) VALUES (?,?)
|
||||||
ON DUPLICATE KEY UPDATE category_id=VALUES(category_id)")
|
ON DUPLICATE KEY UPDATE category_id=VALUES(category_id)")
|
||||||
->execute([$norm, $category_id]);
|
->execute([$norm, $category_id]);
|
||||||
// Also learn individual meaningful words
|
|
||||||
foreach (explode(' ', $norm) as $w) {
|
foreach (explode(' ', $norm) as $w) {
|
||||||
if (strlen($w) >= 4) {
|
if (strlen($w) >= 4) {
|
||||||
$pdo->prepare("INSERT IGNORE INTO pf_item_category_rules (keyword, category_id) VALUES (?,?)")
|
$pdo->prepare("INSERT IGNORE INTO pf_item_category_rules (keyword, category_id) VALUES (?,?)")
|
||||||
@@ -298,7 +271,6 @@ if ($action === 'set_category' && $method === 'POST') {
|
|||||||
|
|
||||||
$pdo->prepare("UPDATE pf_grocery_items SET category_id=? WHERE id=?")->execute([$category_id, $item_id]);
|
$pdo->prepare("UPDATE pf_grocery_items SET category_id=? WHERE id=?")->execute([$category_id, $item_id]);
|
||||||
|
|
||||||
// Learn from manual assignment
|
|
||||||
if ($category_id !== null) {
|
if ($category_id !== null) {
|
||||||
$row = $pdo->prepare("SELECT label FROM pf_grocery_items WHERE id=?");
|
$row = $pdo->prepare("SELECT label FROM pf_grocery_items WHERE id=?");
|
||||||
$row->execute([$item_id]);
|
$row->execute([$item_id]);
|
||||||
@@ -313,23 +285,32 @@ if ($action === 'set_category' && $method === 'POST') {
|
|||||||
if ($action === 'lists') {
|
if ($action === 'lists') {
|
||||||
if ($method === 'GET') {
|
if ($method === 'GET') {
|
||||||
$firstId = liste_ensure_default($pdo);
|
$firstId = liste_ensure_default($pdo);
|
||||||
$rows = $pdo->query("SELECT id, name, position FROM pf_lists ORDER BY position, id")->fetchAll(PDO::FETCH_ASSOC);
|
$rows = $pdo->query("SELECT id, name, color, list_type, position FROM pf_lists ORDER BY position, id")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
echo json_encode(['lists' => $rows, 'default_id' => $firstId]);
|
echo json_encode(['lists' => $rows, 'default_id' => $firstId]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
if ($method === 'POST') {
|
if ($method === 'POST') {
|
||||||
$name = mb_substr(trim($body['name'] ?? ''), 0, 100);
|
$name = mb_substr(trim($body['name'] ?? ''), 0, 100);
|
||||||
|
$color = mb_substr(trim($body['color'] ?? ''), 0, 20) ?: null;
|
||||||
|
$list_type = mb_substr(trim($body['list_type'] ?? ''), 0, 50) ?: null;
|
||||||
if (!$name) { http_response_code(400); echo json_encode(['error' => 'name required']); exit; }
|
if (!$name) { http_response_code(400); echo json_encode(['error' => 'name required']); exit; }
|
||||||
$pos = (int)$pdo->query("SELECT COALESCE(MAX(position),0)+1 FROM pf_lists")->fetchColumn();
|
$pos = (int)$pdo->query("SELECT COALESCE(MAX(position),0)+1 FROM pf_lists")->fetchColumn();
|
||||||
$pdo->prepare("INSERT INTO pf_lists (name, position) VALUES (?,?)")->execute([$name, $pos]);
|
$pdo->prepare("INSERT INTO pf_lists (name, color, list_type, position) VALUES (?,?,?,?)")->execute([$name, $color, $list_type, $pos]);
|
||||||
echo json_encode(['id' => (int)$pdo->lastInsertId(), 'name' => $name, 'position' => $pos]);
|
echo json_encode(['id' => (int)$pdo->lastInsertId(), 'name' => $name, 'color' => $color, 'list_type' => $list_type, 'position' => $pos]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
if ($method === 'PUT') {
|
if ($method === 'PUT') {
|
||||||
$id = (int)($_GET['id'] ?? 0);
|
$id = (int)($_GET['id'] ?? 0);
|
||||||
$name = mb_substr(trim($body['name'] ?? ''), 0, 100);
|
$name = mb_substr(trim($body['name'] ?? ''), 0, 100);
|
||||||
|
$color = array_key_exists('color', $body) ? (mb_substr(trim($body['color'] ?? ''), 0, 20) ?: null) : false;
|
||||||
|
$list_type = array_key_exists('list_type', $body) ? (mb_substr(trim($body['list_type'] ?? ''), 0, 50) ?: null) : false;
|
||||||
if (!$id || !$name) { http_response_code(400); echo json_encode(['error' => 'invalid']); exit; }
|
if (!$id || !$name) { http_response_code(400); echo json_encode(['error' => 'invalid']); exit; }
|
||||||
$pdo->prepare("UPDATE pf_lists SET name=? WHERE id=?")->execute([$name, $id]);
|
$sets = ['name=?'];
|
||||||
|
$vals = [$name];
|
||||||
|
if ($color !== false) { $sets[] = 'color=?'; $vals[] = $color; }
|
||||||
|
if ($list_type !== false) { $sets[] = 'list_type=?'; $vals[] = $list_type; }
|
||||||
|
$vals[] = $id;
|
||||||
|
$pdo->prepare("UPDATE pf_lists SET " . implode(', ', $sets) . " WHERE id=?")->execute($vals);
|
||||||
echo json_encode(['ok' => true]); exit;
|
echo json_encode(['ok' => true]); exit;
|
||||||
}
|
}
|
||||||
if ($method === 'DELETE') {
|
if ($method === 'DELETE') {
|
||||||
@@ -419,7 +400,7 @@ if ($action === 'clear_all' && $method === 'POST') {
|
|||||||
// ── HISTORY ───────────────────────────────────────────────────────────────────
|
// ── HISTORY ───────────────────────────────────────────────────────────────────
|
||||||
if ($action === 'history' && $method === 'GET') {
|
if ($action === 'history' && $method === 'GET') {
|
||||||
$max = 20;
|
$max = 20;
|
||||||
$n = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type='setting' AND reference_id='liste_history_max'"); $n->execute();
|
$n = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type='grocery_settings' AND reference_id='history_max'"); $n->execute();
|
||||||
if ($r = $n->fetch()) $max = max(1, min(50, (int)$r['content']));
|
if ($r = $n->fetch()) $max = max(1, min(50, (int)$r['content']));
|
||||||
$s = $pdo->query("SELECT label_display FROM pf_grocery_history ORDER BY last_used_at DESC LIMIT $max");
|
$s = $pdo->query("SELECT label_display FROM pf_grocery_history ORDER BY last_used_at DESC LIMIT $max");
|
||||||
echo json_encode(['history' => $s->fetchAll(PDO::FETCH_COLUMN)]); exit;
|
echo json_encode(['history' => $s->fetchAll(PDO::FETCH_COLUMN)]); exit;
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
// Désactive l'affichage des erreurs HTML pour garantir un retour JSON strict
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
error_reporting(0);
|
||||||
|
|
||||||
|
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||||
|
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||||
|
require dirname(__DIR__, 4) . '/includes/i18n.php';
|
||||||
|
require_login();
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$dateParam = $_GET['date'] ?? date('Y-m-d');
|
||||||
|
$baseDate = new DateTime($dateParam);
|
||||||
|
|
||||||
|
// Aligner sur le lundi de la semaine
|
||||||
|
$monday = clone $baseDate;
|
||||||
|
if ($monday->format('N') != 1) {
|
||||||
|
$monday->modify('last monday');
|
||||||
|
}
|
||||||
|
|
||||||
|
$sunday = clone $monday;
|
||||||
|
$sunday->modify('+6 days');
|
||||||
|
|
||||||
|
$weekData = [
|
||||||
|
'week_label' => tr('meal_week') . ' ' . $monday->format('W'),
|
||||||
|
'days' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
// Extraction des repas de la base (person_id = 0 pour le foyer global)
|
||||||
|
$stmt = $pdo->prepare("SELECT plan_date, service, meal_name FROM pf_meals_plan WHERE plan_date BETWEEN ? AND ? AND person_id = 0");
|
||||||
|
$stmt->execute([$monday->format('Y-m-d'), $sunday->format('Y-m-d')]);
|
||||||
|
|
||||||
|
$meals = [];
|
||||||
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
$meals[$row['plan_date']][$row['service']] = $row['meal_name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$current = clone $monday;
|
||||||
|
|
||||||
|
// Mappage vers les clés i18n existantes de HouseHub
|
||||||
|
$daysMap = [
|
||||||
|
1 => 'day_mon', 2 => 'day_tue', 3 => 'day_wed',
|
||||||
|
4 => 'day_thu', 5 => 'day_fri', 6 => 'day_sat', 7 => 'day_sun'
|
||||||
|
];
|
||||||
|
|
||||||
|
for ($i = 0; $i < 7; $i++) {
|
||||||
|
$dStr = $current->format('Y-m-d');
|
||||||
|
|
||||||
|
$dayNum = $current->format('N'); // 1 à 7
|
||||||
|
$monthNum = $current->format('m'); // 01 à 12
|
||||||
|
$dayDate = $current->format('d'); // 01 à 31
|
||||||
|
|
||||||
|
$dayName = tr($daysMap[$dayNum]);
|
||||||
|
$monthName = tr('month_' . $monthNum);
|
||||||
|
|
||||||
|
$weekData['days'][] = [
|
||||||
|
'date' => $dStr,
|
||||||
|
'day_name' => $dayName,
|
||||||
|
'date_formatted' => $dayDate . ' ' . $monthName,
|
||||||
|
'lunch' => $meals[$dStr]['lunch'] ?? '',
|
||||||
|
'dinner' => $meals[$dStr]['dinner'] ?? ''
|
||||||
|
];
|
||||||
|
$current->modify('+1 day');
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'data' => $weekData], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
<?php
|
||||||
|
// modules/meals/includes/api/import-pdf.php
|
||||||
|
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||||
|
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||||
|
require_login();
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || empty($_FILES['pdf_file'])) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Aucun fichier reçu.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$personId = (int)$_POST['person_id'];
|
||||||
|
if ($personId <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Enfant non spécifié.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// PARSEUR PDF SPATIAL NATIF (100% PHP SANS LIBRAIRIE)
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
class NativePdfParser {
|
||||||
|
public static function getMealData($filename) {
|
||||||
|
$content = file_get_contents($filename);
|
||||||
|
$texts = [];
|
||||||
|
|
||||||
|
// 1. Décompression et Tokenization Mathématique
|
||||||
|
if (preg_match_all('/stream(.*?)endstream/is', $content, $matches)) {
|
||||||
|
foreach ($matches[1] as $stream) {
|
||||||
|
$stream = ltrim($stream, "\r\n");
|
||||||
|
$decoded = @gzuncompress($stream);
|
||||||
|
if (!$decoded) $decoded = $stream;
|
||||||
|
|
||||||
|
// On éclate le flux par opérateurs PDF clés (Position et Texte)
|
||||||
|
$parts = preg_split('/(Tm|Td|Tj|TJ)/', $decoded, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||||
|
$x = 0; $y = 0;
|
||||||
|
|
||||||
|
for ($i = 0; $i < count($parts); $i++) {
|
||||||
|
$token = $parts[$i];
|
||||||
|
if ($token === 'Tm' || $token === 'Td') {
|
||||||
|
$prev = $parts[$i-1];
|
||||||
|
preg_match_all('/[0-9.-]+/', $prev, $nums);
|
||||||
|
if (count($nums[0]) >= 2) {
|
||||||
|
$yRaw = array_pop($nums[0]);
|
||||||
|
$xRaw = array_pop($nums[0]);
|
||||||
|
if ($token === 'Tm') {
|
||||||
|
$x = (float)$xRaw; $y = (float)$yRaw;
|
||||||
|
} else {
|
||||||
|
$x += (float)$xRaw; $y += (float)$yRaw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($token === 'Tj') {
|
||||||
|
$prev = $parts[$i-1];
|
||||||
|
if (preg_match('/\((.*?)\)$/', trim($prev), $m)) {
|
||||||
|
self::addText($texts, $m[1], $x, $y);
|
||||||
|
}
|
||||||
|
} elseif ($token === 'TJ') {
|
||||||
|
$prev = $parts[$i-1];
|
||||||
|
if (preg_match('/\[(.*?)\]$/', trim($prev), $m)) {
|
||||||
|
preg_match_all('/\((.*?)\)/', $m[1], $tjs);
|
||||||
|
$tjX = $x;
|
||||||
|
foreach ($tjs[1] as $tj) {
|
||||||
|
self::addText($texts, $tj, $tjX, $y);
|
||||||
|
$tjX += 10; // Décalage estimé pour les mots suivants du tableau
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($texts)) throw new Exception("Impossible de lire le contenu du PDF. Le fichier est peut-être crypté ou corrompu.");
|
||||||
|
|
||||||
|
// 2. Définition des Colonnes Verticles (X) via les En-têtes de Jour
|
||||||
|
$dayLabels = ['LUNDI', 'MARDI', 'MERCREDI', 'JEUDI', 'VENDREDI'];
|
||||||
|
$colXs = [];
|
||||||
|
foreach ($texts as $t) {
|
||||||
|
$clean = strtoupper(preg_replace('/[^A-Z]/', '', $t['text']));
|
||||||
|
if (in_array($clean, $dayLabels)) {
|
||||||
|
$colXs[$clean] = $t['x'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sécurité si les jours exacts ne sont pas détectés à cause de la police
|
||||||
|
if (count($colXs) < 5) {
|
||||||
|
$allXs = array_column($texts, 'x');
|
||||||
|
sort($allXs);
|
||||||
|
$minX = $allXs[0] ?? 0;
|
||||||
|
$maxX = end($allXs) ?? 500;
|
||||||
|
$step = ($maxX - $minX) / 5;
|
||||||
|
$colXs = [
|
||||||
|
'LUNDI' => $minX + $step * 0.5,
|
||||||
|
'MARDI' => $minX + $step * 1.5,
|
||||||
|
'MERCREDI' => $minX + $step * 2.5,
|
||||||
|
'JEUDI' => $minX + $step * 3.5,
|
||||||
|
'VENDREDI' => $minX + $step * 4.5,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$sortedXs = array_values($colXs);
|
||||||
|
sort($sortedXs);
|
||||||
|
|
||||||
|
// 3. Définition des Tranches Horizontales (Y) via les Titres de Rangée (Zone Extrême Gauche)
|
||||||
|
$rowHeaders = [];
|
||||||
|
foreach ($texts as $t) {
|
||||||
|
$clean = strtoupper(preg_replace('/[^A-Z]/', '', $t['text']));
|
||||||
|
if (in_array($clean, ['HORSDEUVRE', 'DECLINAISON', 'PLATCHAUD', 'GARNITURE', 'PRODUITLAITIER', 'DESSERT', 'LESGOUTERS'])) {
|
||||||
|
// On s'assure de ne capter que la colonne tout à gauche
|
||||||
|
if ($t['x'] < ($sortedXs[0] - 20)) {
|
||||||
|
$rowHeaders[] = ['label' => $clean, 'y' => $t['y']];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tri de haut en bas (Y du PDF est décroissant du haut vers le bas)
|
||||||
|
usort($rowHeaders, fn($a, $b) => $b['y'] <=> $a['y']);
|
||||||
|
|
||||||
|
$yPlatTop = null; $yPlatBot = null;
|
||||||
|
$yGarTop = null; $yGarBot = null;
|
||||||
|
|
||||||
|
for ($i = 0; $i < count($rowHeaders); $i++) {
|
||||||
|
if ($rowHeaders[$i]['label'] === 'PLATCHAUD') {
|
||||||
|
$yPlatTop = $rowHeaders[$i]['y'];
|
||||||
|
// La limite basse est le label suivant, ou -50 unités
|
||||||
|
$yPlatBot = $rowHeaders[$i+1]['y'] ?? ($yPlatTop - 50);
|
||||||
|
}
|
||||||
|
if ($rowHeaders[$i]['label'] === 'GARNITURE') {
|
||||||
|
$yGarTop = $rowHeaders[$i]['y'];
|
||||||
|
$yGarBot = $rowHeaders[$i+1]['y'] ?? ($yGarTop - 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$yPlatTop || !$yGarTop) {
|
||||||
|
throw new Exception("Structure du menu non reconnue (Mots-clés 'PLAT CHAUD' et 'GARNITURE' introuvables).");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Affectation Chirurgicale par Intersection Grille (X, Y)
|
||||||
|
$days = [[], [], [], [], []];
|
||||||
|
foreach ($texts as $t) {
|
||||||
|
$cleanAlpha = strtoupper(preg_replace('/[^A-Z]/', '', $t['text']));
|
||||||
|
|
||||||
|
// Rejet des labels génériques et de la fameuse ligne "Déclinaison"
|
||||||
|
if (in_array($cleanAlpha, ['DECLINAISON', 'MENUVEGETARIEN', 'PLATCHAUD', 'GARNITURE', 'PRODUITLAITIER', 'HORSDEUVRE', 'LESGOUTERS', 'DESSERT'])) continue;
|
||||||
|
|
||||||
|
// Retrait des labels BIO/HVE et mentions superflues
|
||||||
|
$textVal = trim(preg_replace('/\b(AB|HVE|MSC|Label Rouge)\b/i', '', $t['text']));
|
||||||
|
$textVal = str_replace(['(plat', 'complet)'], '', $textVal);
|
||||||
|
$textVal = trim(preg_replace('/[^a-zA-ZÀ-ÿ0-9\s&\-]/u', '', $textVal));
|
||||||
|
|
||||||
|
if (strlen($textVal) < 3) continue;
|
||||||
|
|
||||||
|
// Filtre Vertical : Est-on dans la tranche Plat Chaud ou Garniture ? (Tolérance +/- 5 unités)
|
||||||
|
$inPlat = ($t['y'] <= $yPlatTop + 5 && $t['y'] >= $yPlatBot + 5);
|
||||||
|
$inGar = ($t['y'] <= $yGarTop + 5 && $t['y'] >= $yGarBot + 5);
|
||||||
|
|
||||||
|
if ($inPlat || $inGar) {
|
||||||
|
// Filtre Horizontal : Quel est le jour le plus proche ?
|
||||||
|
$bestCol = 0;
|
||||||
|
$minDiff = PHP_FLOAT_MAX;
|
||||||
|
foreach ($sortedXs as $idx => $cx) {
|
||||||
|
$diff = abs($t['x'] - $cx);
|
||||||
|
if ($diff < $minDiff) {
|
||||||
|
$minDiff = $diff;
|
||||||
|
$bestCol = $idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$days[$bestCol][] = ['text' => $textVal, 'y' => $t['y'], 'type' => $inPlat ? 'plat' : 'gar'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Concaténation Finale
|
||||||
|
$finalDays = [];
|
||||||
|
for ($i = 0; $i < 5; $i++) {
|
||||||
|
$plats = array_filter($days[$i], fn($item) => $item['type'] === 'plat');
|
||||||
|
$gars = array_filter($days[$i], fn($item) => $item['type'] === 'gar');
|
||||||
|
|
||||||
|
usort($plats, fn($a, $b) => $b['y'] <=> $a['y']);
|
||||||
|
usort($gars, fn($a, $b) => $b['y'] <=> $a['y']);
|
||||||
|
|
||||||
|
$platText = self::concatLines($plats);
|
||||||
|
$garText = self::concatLines($gars);
|
||||||
|
|
||||||
|
$full = trim("$platText + $garText", " +");
|
||||||
|
$finalDays[] = preg_replace('/\s+/', ' ', $full);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $finalDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function concatLines($items) {
|
||||||
|
$lines = [];
|
||||||
|
$currentLine = "";
|
||||||
|
$lastY = null;
|
||||||
|
foreach ($items as $item) {
|
||||||
|
// Si le décalage Y est trop fort, on considère que c'est une nouvelle ligne
|
||||||
|
if ($lastY !== null && abs($lastY - $item['y']) > 8) {
|
||||||
|
if ($currentLine) $lines[] = trim($currentLine);
|
||||||
|
$currentLine = $item['text'];
|
||||||
|
} else {
|
||||||
|
$currentLine .= " " . $item['text'];
|
||||||
|
}
|
||||||
|
$lastY = $item['y'];
|
||||||
|
}
|
||||||
|
if ($currentLine) $lines[] = trim($currentLine);
|
||||||
|
return implode(' ', array_filter($lines, fn($l) => strlen(trim($l)) > 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function addText(&$texts, $rawText, $x, $y) {
|
||||||
|
// Décodage de l'octal \123
|
||||||
|
$m = preg_replace_callback('/\\\\([0-7]{1,3})/', function($cb) { return chr(octdec($cb[1])); }, $rawText);
|
||||||
|
|
||||||
|
// Détection de l'UTF-16BE (Le BOM \xFE\xFF indique que le texte est double encodé)
|
||||||
|
if (str_starts_with($m, "\xFE\xFF")) {
|
||||||
|
$m = mb_convert_encoding(substr($m, 2), 'UTF-8', 'UTF-16BE');
|
||||||
|
} else {
|
||||||
|
// Fallback Latin-1 si pas de BOM
|
||||||
|
$m = mb_convert_encoding($m, 'UTF-8', 'ISO-8859-1');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suppression radicale des octets nuls responsables de l'espacement forcé (E s c a l o p e)
|
||||||
|
$m = str_replace("\0", "", $m);
|
||||||
|
$m = trim(preg_replace('/[^\p{L}\p{N}\p{P}\p{Z}\+&\'’\-]/u', '', $m));
|
||||||
|
|
||||||
|
if ($m !== '') {
|
||||||
|
$texts[] = ['text' => $m, 'x' => $x, 'y' => $y];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$filename = $_FILES['pdf_file']['name'];
|
||||||
|
|
||||||
|
// 1. Extraction robuste de la date depuis le nom de fichier (Menus-du-14-au-20-septembre-2026.pdf)
|
||||||
|
$mondayDate = null;
|
||||||
|
if (preg_match('/(\d{1,2})[\s-]+au[\s-]+\d{1,2}[\s-]+([a-zèéû]+)[\s-]+(\d{4})/i', $filename, $m)) {
|
||||||
|
$monthsMap = ['janvier'=>'01','fevrier'=>'02','février'=>'02','mars'=>'03','avril'=>'04','mai'=>'05','juin'=>'06','juillet'=>'07','aout'=>'08','août'=>'08','septembre'=>'09','octobre'=>'10','novembre'=>'11','decembre'=>'12','décembre'=>'12'];
|
||||||
|
$month = $monthsMap[strtolower($m[2])] ?? '09';
|
||||||
|
$mondayDate = $m[3] . '-' . $month . '-' . str_pad($m[1], 2, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$mondayDate) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Veuillez nommer le fichier "Menus-du-14-au-20-septembre-2026.pdf".']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Lancement de l'Analyse Spatiale
|
||||||
|
$plats = NativePdfParser::getMealData($_FILES['pdf_file']['tmp_name']);
|
||||||
|
|
||||||
|
if (empty($plats) || count($plats) !== 5) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Erreur lors du découpage des 5 colonnes.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Insertion en BDD (Lundi au Vendredi)
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO pf_meals_plan (plan_date, service, person_id, meal_name)
|
||||||
|
VALUES (?, 'lunch', ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE meal_name = VALUES(meal_name)
|
||||||
|
");
|
||||||
|
|
||||||
|
$currentDate = new DateTime($mondayDate);
|
||||||
|
for ($i = 0; $i < 5; $i++) {
|
||||||
|
$fullMeal = $plats[$i];
|
||||||
|
if (!empty($fullMeal) && $fullMeal !== '+') {
|
||||||
|
$stmt->execute([$currentDate->format('Y-m-d'), $personId, $fullMeal]);
|
||||||
|
}
|
||||||
|
$currentDate->modify('+1 day');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||||
|
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||||
|
require_login();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Méthode non autorisée']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = $_POST['action'] ?? '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($action === 'save_cell') {
|
||||||
|
$date = $_POST['plan_date'];
|
||||||
|
$service = $_POST['service'];
|
||||||
|
$person_id = (int)$_POST['person_id'];
|
||||||
|
$meal_name = trim($_POST['meal_name']);
|
||||||
|
|
||||||
|
if (empty($meal_name)) {
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM pf_meals_plan WHERE plan_date=? AND service=? AND person_id=?");
|
||||||
|
$stmt->execute([$date, $service, $person_id]);
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pf_meals_plan (plan_date, service, person_id, meal_name) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE meal_name = VALUES(meal_name)");
|
||||||
|
$stmt->execute([$date, $service, $person_id, $meal_name]);
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'save_bulk') {
|
||||||
|
$updates = json_decode($_POST['updates'], true);
|
||||||
|
if (is_array($updates)) {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
$stmtDel = $pdo->prepare("DELETE FROM pf_meals_plan WHERE plan_date=? AND service=? AND person_id=?");
|
||||||
|
$stmtIns = $pdo->prepare("INSERT INTO pf_meals_plan (plan_date, service, person_id, meal_name) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE meal_name = VALUES(meal_name)");
|
||||||
|
|
||||||
|
foreach ($updates as $u) {
|
||||||
|
if (empty(trim($u['meal_name']))) {
|
||||||
|
$stmtDel->execute([$u['plan_date'], $u['service'], $u['person_id']]);
|
||||||
|
} else {
|
||||||
|
$stmtIns->execute([$u['plan_date'], $u['service'], $u['person_id'], trim($u['meal_name'])]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$pdo->commit();
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -1,5 +1,53 @@
|
|||||||
/* Liste module JS */
|
<?php
|
||||||
const API = '/modules/liste/api.php';
|
// modules/food/views/liste.php
|
||||||
|
if (!isset($pdo)) { exit; }
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="liste-main">
|
||||||
|
<!-- Tabs des listes -->
|
||||||
|
<div class="liste-tabs-bar">
|
||||||
|
<div class="liste-tabs" id="liste-tabs"></div>
|
||||||
|
<button class="liste-tab-add" id="btn-add-list" title="<?= htmlspecialchars(tr('liste_new_list')) ?>">+</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Corps de la liste active -->
|
||||||
|
<div class="liste-body" id="liste-body">
|
||||||
|
<div class="liste-add-card">
|
||||||
|
<input type="text" id="liste-input" maxlength="500" placeholder="<?= htmlspecialchars(tr('liste_placeholder_add')) ?>" autocomplete="off">
|
||||||
|
<button id="btn-liste-add"><?= htmlspecialchars(tr('liste_btn_add')) ?></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="liste-toolbar" id="liste-toolbar">
|
||||||
|
<button class="btn-tool" id="btn-uncheck-all"><?= htmlspecialchars(tr('liste_btn_uncheck_all')) ?></button>
|
||||||
|
<button class="btn-tool btn-tool-danger" id="btn-delete-picked"><?= htmlspecialchars(tr('liste_btn_delete_picked')) ?></button>
|
||||||
|
<button class="btn-tool btn-tool-danger" id="btn-clear-all"><?= htmlspecialchars(tr('liste_btn_clear_all')) ?></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="liste-items-root"></div>
|
||||||
|
<div id="liste-history-root"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const LISTE_TRANSLATIONS = {
|
||||||
|
already_in: <?= json_encode(tr('liste_already_in')) ?>,
|
||||||
|
confirm_clear: <?= json_encode(tr('liste_confirm_clear')) ?>,
|
||||||
|
confirm_delete_list: <?= json_encode(tr('liste_confirm_delete_list')) ?>,
|
||||||
|
rename_list: <?= json_encode(tr('liste_rename_list')) ?>,
|
||||||
|
new_name: <?= json_encode(tr('liste_new_name')) ?>,
|
||||||
|
list_deleted: <?= json_encode(tr('liste_list_deleted')) ?>,
|
||||||
|
list_created: <?= json_encode(tr('liste_list_created')) ?>,
|
||||||
|
list_renamed: <?= json_encode(tr('liste_list_renamed')) ?>,
|
||||||
|
edit_placeholder: <?= json_encode(tr('liste_edit_placeholder')) ?>,
|
||||||
|
history_title: <?= json_encode(tr('liste_history_title')) ?>,
|
||||||
|
added: <?= json_encode(tr('liste_added')) ?>,
|
||||||
|
updated: <?= json_encode(tr('liste_updated')) ?>,
|
||||||
|
deleted: <?= json_encode(tr('liste_deleted')) ?>,
|
||||||
|
cancel: <?= json_encode(tr('cancel')) ?>,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ⚠️ NOUVEAU CHEMIN D'API
|
||||||
|
const API = '/modules/food/includes/api/api-liste.php';
|
||||||
const T = window.LISTE_TRANSLATIONS || {};
|
const T = window.LISTE_TRANSLATIONS || {};
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
@@ -11,6 +59,35 @@ const state = {
|
|||||||
catMap: {},
|
catMap: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Constants ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const LIST_COLORS = [
|
||||||
|
{ id: 'none', hex: null },
|
||||||
|
{ id: 'blue', hex: '#3b82f6' },
|
||||||
|
{ id: 'red', hex: '#ef4444' },
|
||||||
|
{ id: 'amber', hex: '#f59e0b' },
|
||||||
|
{ id: 'purple', hex: '#8b5cf6' },
|
||||||
|
{ id: 'pink', hex: '#ec4899' },
|
||||||
|
{ id: 'emerald', hex: '#10b981' },
|
||||||
|
{ id: 'orange', hex: '#f97316' },
|
||||||
|
{ id: 'cyan', hex: '#06b6d4' },
|
||||||
|
{ id: 'indigo', hex: '#6366f1' },
|
||||||
|
{ id: 'teal', hex: '#14b8a6' },
|
||||||
|
{ id: 'gray', hex: '#6b7280' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const LIST_TYPES = [
|
||||||
|
{ value: '', label: '— Aucun —', emoji: '' },
|
||||||
|
{ value: 'courses', label: 'Courses', emoji: '🛒' },
|
||||||
|
{ value: 'todo', label: 'To-do', emoji: '✅' },
|
||||||
|
{ value: 'voyage', label: 'Voyage', emoji: '✈️' },
|
||||||
|
{ value: 'travail', label: 'Travail', emoji: '💼' },
|
||||||
|
{ value: 'maison', label: 'Maison', emoji: '🏠' },
|
||||||
|
{ value: 'sante', label: 'Santé', emoji: '💊' },
|
||||||
|
{ value: 'loisirs', label: 'Loisirs', emoji: '🎮' },
|
||||||
|
{ value: 'autre', label: 'Autre', emoji: '📋' },
|
||||||
|
];
|
||||||
|
|
||||||
// ── Utilities ──────────────────────────────────────────────────────────────────
|
// ── Utilities ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function esc(s) {
|
function esc(s) {
|
||||||
@@ -42,6 +119,116 @@ async function api(action, method = 'GET', body = null, params = {}) {
|
|||||||
return r.json();
|
return r.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── List modal ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let activeModal = null;
|
||||||
|
|
||||||
|
function openListModal(listId = null) {
|
||||||
|
if (activeModal) { activeModal.remove(); activeModal = null; }
|
||||||
|
const list = listId ? state.lists.find(l => l.id === listId) : null;
|
||||||
|
const isNew = !listId;
|
||||||
|
|
||||||
|
const backdrop = document.createElement('div');
|
||||||
|
backdrop.className = 'liste-modal-backdrop';
|
||||||
|
backdrop.innerHTML = `
|
||||||
|
<div class="liste-modal" role="dialog" aria-modal="true">
|
||||||
|
<div class="liste-modal-header">
|
||||||
|
<h3>${isNew ? 'Nouvelle liste' : 'Modifier la liste'}</h3>
|
||||||
|
<button class="liste-modal-close" aria-label="Fermer">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="liste-modal-body">
|
||||||
|
<div class="liste-form-group">
|
||||||
|
<label class="liste-form-label">Nom</label>
|
||||||
|
<input class="liste-modal-name" type="text" maxlength="100"
|
||||||
|
value="${esc(list?.name ?? '')}" placeholder="Ma liste…" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="liste-form-group">
|
||||||
|
<label class="liste-form-label">Couleur</label>
|
||||||
|
<div class="liste-color-picker">
|
||||||
|
${LIST_COLORS.map(c => {
|
||||||
|
const isActive = (c.hex === null && !list?.color) || (list?.color === c.hex);
|
||||||
|
const cls = 'liste-color-swatch' + (c.hex === null ? ' liste-color-swatch-none' : '') + (isActive ? ' active' : '');
|
||||||
|
const style = c.hex ? `style="--swatch:${c.hex}"` : '';
|
||||||
|
return `<button class="${cls}" data-color="${c.hex ?? ''}" ${style} title="${c.id}"></button>`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="liste-form-group">
|
||||||
|
<label class="liste-form-label">Type de liste</label>
|
||||||
|
<select class="liste-modal-type">
|
||||||
|
${LIST_TYPES.map(t =>
|
||||||
|
`<option value="${esc(t.value)}" ${list?.list_type === t.value ? 'selected' : ''}>${t.emoji ? t.emoji + ' ' : ''}${esc(t.label)}</option>`
|
||||||
|
).join('')}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="liste-modal-footer">
|
||||||
|
${!isNew && state.lists.length > 1
|
||||||
|
? `<button class="btn-tool btn-tool-danger liste-modal-delete">Supprimer</button>`
|
||||||
|
: ''}
|
||||||
|
<button class="btn-tool liste-modal-cancel">Annuler</button>
|
||||||
|
<button class="btn-tool btn-tool-primary liste-modal-save">Enregistrer</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
document.body.appendChild(backdrop);
|
||||||
|
activeModal = backdrop;
|
||||||
|
|
||||||
|
const nameInput = backdrop.querySelector('.liste-modal-name');
|
||||||
|
nameInput.focus();
|
||||||
|
nameInput.setSelectionRange(nameInput.value.length, nameInput.value.length);
|
||||||
|
|
||||||
|
// Color swatches
|
||||||
|
backdrop.querySelectorAll('.liste-color-swatch').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
backdrop.querySelectorAll('.liste-color-swatch').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const close = () => { backdrop.remove(); activeModal = null; };
|
||||||
|
backdrop.querySelector('.liste-modal-close').addEventListener('click', close);
|
||||||
|
backdrop.querySelector('.liste-modal-cancel').addEventListener('click', close);
|
||||||
|
backdrop.addEventListener('click', e => { if (e.target === backdrop) close(); });
|
||||||
|
|
||||||
|
backdrop.querySelector('.liste-modal-delete')?.addEventListener('click', () => { close(); deleteList(listId); });
|
||||||
|
backdrop.querySelector('.liste-modal-save').addEventListener('click', () => saveListModal(backdrop, listId));
|
||||||
|
nameInput.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Enter') saveListModal(backdrop, listId);
|
||||||
|
if (e.key === 'Escape') close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveListModal(backdrop, listId) {
|
||||||
|
const name = backdrop.querySelector('.liste-modal-name').value.trim();
|
||||||
|
if (!name) { backdrop.querySelector('.liste-modal-name').focus(); return; }
|
||||||
|
|
||||||
|
const activeSwatch = backdrop.querySelector('.liste-color-swatch.active');
|
||||||
|
const color = activeSwatch?.dataset.color || null;
|
||||||
|
const list_type = backdrop.querySelector('.liste-modal-type').value || null;
|
||||||
|
|
||||||
|
if (listId) {
|
||||||
|
const r = await api('lists', 'PUT', { name, color, list_type }, { id: listId });
|
||||||
|
if (r.ok) {
|
||||||
|
const list = state.lists.find(l => l.id === listId);
|
||||||
|
if (list) { list.name = name; list.color = color; list.list_type = list_type; }
|
||||||
|
backdrop.remove(); activeModal = null;
|
||||||
|
renderTabs();
|
||||||
|
toast('Liste mise à jour');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const r = await api('lists', 'POST', { name, color, list_type });
|
||||||
|
if (r.id) {
|
||||||
|
state.lists.push({ id: r.id, name, color: r.color, list_type: r.list_type, position: r.position });
|
||||||
|
state.currentListId = r.id;
|
||||||
|
backdrop.remove(); activeModal = null;
|
||||||
|
await loadItems();
|
||||||
|
renderTabs();
|
||||||
|
toast('Liste créée : ' + name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Categories ─────────────────────────────────────────────────────────────────
|
// ── Categories ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function loadCategories() {
|
async function loadCategories() {
|
||||||
@@ -104,49 +291,54 @@ function closeCategoryPicker() {
|
|||||||
|
|
||||||
// ── Tabs ───────────────────────────────────────────────────────────────────────
|
// ── Tabs ───────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function typeEmoji(list_type) {
|
||||||
|
return LIST_TYPES.find(t => t.value === list_type)?.emoji || '';
|
||||||
|
}
|
||||||
|
|
||||||
function renderTabs() {
|
function renderTabs() {
|
||||||
const container = document.getElementById('liste-tabs');
|
const container = document.getElementById('liste-tabs');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
state.lists.forEach(list => {
|
state.lists.forEach(list => {
|
||||||
|
const isActive = list.id === state.currentListId;
|
||||||
const tab = document.createElement('div');
|
const tab = document.createElement('div');
|
||||||
tab.className = 'liste-tab' + (list.id === state.currentListId ? ' active' : '');
|
tab.className = 'liste-tab' + (isActive ? ' active' : '');
|
||||||
tab.dataset.id = list.id;
|
tab.dataset.id = list.id;
|
||||||
|
|
||||||
if (list.id === state.currentListId) {
|
// Apply custom color to entire tab
|
||||||
|
if (list.color) {
|
||||||
|
if (isActive) {
|
||||||
|
tab.style.cssText = `background:${list.color};border-color:${list.color};color:#fff`;
|
||||||
|
} else {
|
||||||
|
tab.style.cssText = `border-color:${list.color};color:${list.color}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const emoji = typeEmoji(list.list_type);
|
||||||
|
const prefix = emoji ? `<span class="liste-tab-type">${emoji}</span>` : '';
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
tab.innerHTML = `
|
tab.innerHTML = `
|
||||||
|
${prefix}
|
||||||
<span class="liste-tab-name">${esc(list.name)}</span>
|
<span class="liste-tab-name">${esc(list.name)}</span>
|
||||||
<button class="liste-tab-btn liste-tab-rename" title="${esc(T.rename_list)}" data-id="${list.id}">✏</button>
|
<button class="liste-tab-btn liste-tab-edit" title="Modifier" data-id="${list.id}">✏</button>
|
||||||
${state.lists.length > 1 ? `<button class="liste-tab-btn liste-tab-delete" title="Supprimer" data-id="${list.id}">×</button>` : ''}
|
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
tab.innerHTML = `<span class="liste-tab-name">${esc(list.name)}</span>`;
|
tab.innerHTML = `${prefix}<span class="liste-tab-name">${esc(list.name)}</span>`;
|
||||||
tab.addEventListener('click', () => switchList(list.id));
|
tab.addEventListener('click', () => switchList(list.id));
|
||||||
}
|
}
|
||||||
container.appendChild(tab);
|
container.appendChild(tab);
|
||||||
});
|
});
|
||||||
|
|
||||||
container.querySelectorAll('.liste-tab-rename').forEach(btn => {
|
container.querySelectorAll('.liste-tab-edit').forEach(btn => {
|
||||||
btn.addEventListener('click', e => { e.stopPropagation(); startRename(parseInt(btn.dataset.id)); });
|
btn.addEventListener('click', e => { e.stopPropagation(); openListModal(parseInt(btn.dataset.id)); });
|
||||||
});
|
|
||||||
container.querySelectorAll('.liste-tab-delete').forEach(btn => {
|
|
||||||
btn.addEventListener('click', e => { e.stopPropagation(); deleteList(parseInt(btn.dataset.id)); });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function startRename(listId) {
|
// ── List management ─────────────────────────────────────────────────────────────
|
||||||
const list = state.lists.find(l => l.id === listId);
|
|
||||||
if (!list) return;
|
document.getElementById('btn-add-list')?.addEventListener('click', () => openListModal(null));
|
||||||
const name = prompt(T.new_name || 'Nouveau nom :', list.name);
|
|
||||||
if (name === null || name.trim() === '') return;
|
|
||||||
api('lists', 'PUT', { name: name.trim() }, { id: listId }).then(r => {
|
|
||||||
if (r.ok) {
|
|
||||||
list.name = name.trim();
|
|
||||||
renderTabs();
|
|
||||||
toast(T.list_renamed || 'Liste renommée');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteList(listId) {
|
async function deleteList(listId) {
|
||||||
if (!confirm(T.confirm_delete_list || 'Supprimer cette liste et tous ses articles ?')) return;
|
if (!confirm(T.confirm_delete_list || 'Supprimer cette liste et tous ses articles ?')) return;
|
||||||
@@ -161,41 +353,6 @@ async function deleteList(listId) {
|
|||||||
toast(T.list_deleted || 'Liste supprimée');
|
toast(T.list_deleted || 'Liste supprimée');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── List management ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
document.getElementById('btn-add-list')?.addEventListener('click', () => {
|
|
||||||
document.getElementById('liste-new-form')?.classList.remove('hidden');
|
|
||||||
document.getElementById('btn-add-list')?.classList.add('hidden');
|
|
||||||
document.getElementById('new-list-name')?.focus();
|
|
||||||
});
|
|
||||||
document.getElementById('btn-cancel-new-list')?.addEventListener('click', cancelNewList);
|
|
||||||
document.getElementById('btn-confirm-new-list')?.addEventListener('click', confirmNewList);
|
|
||||||
document.getElementById('new-list-name')?.addEventListener('keydown', e => {
|
|
||||||
if (e.key === 'Enter') confirmNewList();
|
|
||||||
if (e.key === 'Escape') cancelNewList();
|
|
||||||
});
|
|
||||||
|
|
||||||
function cancelNewList() {
|
|
||||||
document.getElementById('liste-new-form')?.classList.add('hidden');
|
|
||||||
document.getElementById('btn-add-list')?.classList.remove('hidden');
|
|
||||||
if (document.getElementById('new-list-name')) document.getElementById('new-list-name').value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function confirmNewList() {
|
|
||||||
const input = document.getElementById('new-list-name');
|
|
||||||
const name = input?.value.trim();
|
|
||||||
if (!name) return;
|
|
||||||
const r = await api('lists', 'POST', { name });
|
|
||||||
if (r.id) {
|
|
||||||
state.lists.push({ id: r.id, name: r.name, position: r.position });
|
|
||||||
state.currentListId = r.id;
|
|
||||||
cancelNewList();
|
|
||||||
await loadItems();
|
|
||||||
renderTabs();
|
|
||||||
toast((T.list_created || 'Liste créée') + ' : ' + r.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Switch list ─────────────────────────────────────────────────────────────────
|
// ── Switch list ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function switchList(listId) {
|
async function switchList(listId) {
|
||||||
@@ -207,10 +364,7 @@ async function switchList(listId) {
|
|||||||
// ── Items ───────────────────────────────────────────────────────────────────────
|
// ── Items ───────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function loadItems() {
|
async function loadItems() {
|
||||||
if (!state.currentListId) {
|
if (!state.currentListId) { renderItems(); return; }
|
||||||
renderItems();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const r = await api('items', 'GET', null, { list_id: state.currentListId });
|
const r = await api('items', 'GET', null, { list_id: state.currentListId });
|
||||||
state.items = r.items || [];
|
state.items = r.items || [];
|
||||||
renderItems();
|
renderItems();
|
||||||
@@ -236,15 +390,12 @@ function renderItems() {
|
|||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
if (hasCategories && state.categories.length) {
|
if (hasCategories && state.categories.length) {
|
||||||
// Group pending items by category
|
|
||||||
const groups = new Map();
|
const groups = new Map();
|
||||||
pending.forEach(item => {
|
pending.forEach(item => {
|
||||||
const key = item.category_id ?? 0;
|
const key = item.category_id ?? 0;
|
||||||
if (!groups.has(key)) groups.set(key, []);
|
if (!groups.has(key)) groups.set(key, []);
|
||||||
groups.get(key).push(item);
|
groups.get(key).push(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Render in category order, uncategorized last
|
|
||||||
const catIds = state.categories.map(c => parseInt(c.id));
|
const catIds = state.categories.map(c => parseInt(c.id));
|
||||||
const orderedKeys = catIds.filter(id => groups.has(id));
|
const orderedKeys = catIds.filter(id => groups.has(id));
|
||||||
if (groups.has(0)) orderedKeys.push(0);
|
if (groups.has(0)) orderedKeys.push(0);
|
||||||
@@ -284,7 +435,7 @@ function rowHtml(item) {
|
|||||||
const checked = item.in_cart ? 'checked' : '';
|
const checked = item.in_cart ? 'checked' : '';
|
||||||
const cls = item.in_cart ? 'liste-row in-cart' : 'liste-row';
|
const cls = item.in_cart ? 'liste-row in-cart' : 'liste-row';
|
||||||
const cat = item.category_id ? state.catMap[item.category_id] : null;
|
const cat = item.category_id ? state.catMap[item.category_id] : null;
|
||||||
const badgeCls = 'liste-cat-badge' + (cat ? '' : ' liste-cat-badge-empty');
|
const badgeCls = 'liste-cat-badge' + (cat ? '' : ' liste-cat-badge-empty');
|
||||||
const badgeIcon = cat ? esc(cat.icon) : '🏷️';
|
const badgeIcon = cat ? esc(cat.icon) : '🏷️';
|
||||||
const badgeTip = cat ? esc(cat.name) : 'Non classé';
|
const badgeTip = cat ? esc(cat.name) : 'Non classé';
|
||||||
return `<div class="${cls}" data-id="${item.id}">
|
return `<div class="${cls}" data-id="${item.id}">
|
||||||
@@ -412,3 +563,5 @@ async function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
<?php
|
||||||
|
// modules/meals/main.php
|
||||||
|
|
||||||
|
// Récupération stricte des membres du foyer (Exclusion des intervenants et proches)
|
||||||
|
$stmt = $pdo->query("SELECT id, name FROM pf_people WHERE role NOT IN ('helper', 'nounou', 'proche_adulte', 'proche_enfant', 'relative') AND is_active = 1 ORDER BY id ASC");
|
||||||
|
$members = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (empty($members)) {
|
||||||
|
echo "<div class='pf-alert pf-alert--warning'>" . tr('meals_no_members') . "</div>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcul des 14 jours (Lundi semaine courante -> Dimanche semaine prochaine)
|
||||||
|
$start_date = new DateTime();
|
||||||
|
$start_date->modify('monday this week');
|
||||||
|
$dates = [];
|
||||||
|
for ($i = 0; $i < 14; $i++) {
|
||||||
|
$d = clone $start_date;
|
||||||
|
$d->modify("+$i days");
|
||||||
|
$dates[] = $d->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmtMeals = $pdo->prepare("SELECT plan_date, service, person_id, meal_name FROM pf_meals_plan WHERE plan_date BETWEEN ? AND ?");
|
||||||
|
$stmtMeals->execute([$dates[0], end($dates)]);
|
||||||
|
$mealsData = [];
|
||||||
|
while ($row = $stmtMeals->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
$mealsData[$row['plan_date']][$row['service']][$row['person_id']] = $row['meal_name'];
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="pf-container meals-container">
|
||||||
|
<div class="meals-header-bar">
|
||||||
|
<h1 class="meals-title">
|
||||||
|
<span class="meals-title-icon">🍽️</span>
|
||||||
|
<span data-i18n="meals_board_title"><?= tr('meals_board_title') ?></span>
|
||||||
|
</h1>
|
||||||
|
<div class="meals-actions">
|
||||||
|
<!-- NOUVEAU : Bouton Import PDF -->
|
||||||
|
<button type="button" class="pf-btn btn-secondary" onclick="document.getElementById('importMenuModal').classList.add('open'); document.body.classList.add('no-scroll');">
|
||||||
|
📄 <?= tr('meal_btn_import_pdf') ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Barre contextuelle (Fixe en bas) -->
|
||||||
|
<div class="context-toolbar" id="contextToolbar">
|
||||||
|
<button class="ctx-btn" onmousedown="event.preventDefault(); applyToAllInRow()">➡️ <span data-i18n="btn_apply_all"><?= tr('btn_apply_all') ?></span></button>
|
||||||
|
<button class="ctx-btn primary" onmousedown="event.preventDefault(); openCopySheetFromCell()">🔄 <span data-i18n="btn_copy_to"><?= tr('btn_copy_to') ?></span></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="meals-wrapper">
|
||||||
|
<?php
|
||||||
|
$weeks = array_chunk($dates, 7);
|
||||||
|
foreach ($weeks as $weekIndex => $weekDates):
|
||||||
|
$startWeekStr = date('d/m', strtotime($weekDates[0]));
|
||||||
|
$endWeekStr = date('d/m', strtotime(end($weekDates)));
|
||||||
|
?>
|
||||||
|
<h2 class="meals-week-title"><?= tr('meal_week') ?> (<?= $startWeekStr ?> - <?= $endWeekStr ?>)</h2>
|
||||||
|
<div class="meals-table-container">
|
||||||
|
<table class="meals-table" id="mealsTable_<?= $weekIndex ?>">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="col-sticky"></th>
|
||||||
|
<?php foreach ($members as $m): ?>
|
||||||
|
<th><?= htmlspecialchars(strtoupper($m['name'])) ?></th>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($weekDates as $dateStr):
|
||||||
|
$dayKey = strtolower(date('D', strtotime($dateStr)));
|
||||||
|
$dayName = tr('day_' . $dayKey);
|
||||||
|
$dayDate = date('d/m', strtotime($dateStr));
|
||||||
|
$fullDayName = tr('day_full_' . $dayKey) . ' ' . $dayDate;
|
||||||
|
?>
|
||||||
|
<tr class="row-lunch">
|
||||||
|
<td class="col-sticky">
|
||||||
|
<span class="day-label"><?= $dayName ?> <?= $dayDate ?></span>
|
||||||
|
<div class="service-icon" onmousedown="event.preventDefault(); openCopySheetFromRow('<?= $dateStr ?>', 'lunch', '<?= addslashes($fullDayName) ?>')" title="<?= tr('meals_copy_lunch') ?>">☀️</div>
|
||||||
|
</td>
|
||||||
|
<?php foreach ($members as $m):
|
||||||
|
$val = $mealsData[$dateStr]['lunch'][$m['id']] ?? '';
|
||||||
|
?>
|
||||||
|
<td class="meal-input-cell">
|
||||||
|
<textarea class="meal-input js-meal-input" data-d="<?= $dateStr ?>" data-s="lunch" data-p="<?= $m['id'] ?>" placeholder="-"><?= htmlspecialchars($val) ?></textarea>
|
||||||
|
</td>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr class="row-dinner">
|
||||||
|
<td class="col-sticky">
|
||||||
|
<span class="day-label invisible"><?= $dayName ?> <?= $dayDate ?></span>
|
||||||
|
<div class="service-icon" onmousedown="event.preventDefault(); openCopySheetFromRow('<?= $dateStr ?>', 'dinner', '<?= addslashes($fullDayName) ?>')" title="<?= tr('meals_copy_dinner') ?>">🌙</div>
|
||||||
|
</td>
|
||||||
|
<?php foreach ($members as $m):
|
||||||
|
$val = $mealsData[$dateStr]['dinner'][$m['id']] ?? '';
|
||||||
|
?>
|
||||||
|
<td class="meal-input-cell">
|
||||||
|
<textarea class="meal-input js-meal-input" data-d="<?= $dateStr ?>" data-s="dinner" data-p="<?= $m['id'] ?>" placeholder="-"><?= htmlspecialchars($val) ?></textarea>
|
||||||
|
</td>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom Sheet de Copie (Format Grille Ultra-compacte) -->
|
||||||
|
<div id="copyModal" class="pf-modal pf-bottom-sheet">
|
||||||
|
<div class="pf-modal-content pf-bs-content copy-modal-large">
|
||||||
|
<div class="bs-header">
|
||||||
|
<h3 class="bs-title">🔄 <span data-i18n="modal_copy_title"><?= tr('modal_copy_title') ?></span></h3>
|
||||||
|
<button class="bs-close" onclick="closeCopyModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div id="copySourceLabel" class="meals-copy-source">Source : ...</div>
|
||||||
|
|
||||||
|
<div class="week-section-title" data-i18n="week_current"><?= tr('week_current') ?></div>
|
||||||
|
<div class="copy-days-grid">
|
||||||
|
<?php for($i=0; $i<7; $i++): $dStr = $dates[$i]; $dName = tr('day_full_' . strtolower(date('D', strtotime($dStr)))) . ' ' . date('d/m', strtotime($dStr)); ?>
|
||||||
|
<div class="copy-day-card">
|
||||||
|
<div class="copy-day-name"><?= $dName ?></div>
|
||||||
|
<div class="copy-day-actions">
|
||||||
|
<button type="button" class="btn-target-service" onmousedown="event.preventDefault(); executeCopy('<?= $dStr ?>', 'lunch')" title="<?= tr('meal_lunch') ?>">☀️</button>
|
||||||
|
<button type="button" class="btn-target-service" onmousedown="event.preventDefault(); executeCopy('<?= $dStr ?>', 'dinner')" title="<?= tr('meal_dinner') ?>">🌙</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="week-section-title week-section-mt" data-i18n="week_next"><?= tr('week_next') ?></div>
|
||||||
|
<div class="copy-days-grid">
|
||||||
|
<?php for($i=7; $i<14; $i++): $dStr = $dates[$i]; $dName = tr('day_full_' . strtolower(date('D', strtotime($dStr)))) . ' ' . date('d/m', strtotime($dStr)); ?>
|
||||||
|
<div class="copy-day-card">
|
||||||
|
<div class="copy-day-name"><?= $dName ?></div>
|
||||||
|
<div class="copy-day-actions">
|
||||||
|
<button type="button" class="btn-target-service" onmousedown="event.preventDefault(); executeCopy('<?= $dStr ?>', 'lunch')" title="<?= tr('meal_lunch') ?>">☀️</button>
|
||||||
|
<button type="button" class="btn-target-service" onmousedown="event.preventDefault(); executeCopy('<?= $dStr ?>', 'dinner')" title="<?= tr('meal_dinner') ?>">🌙</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- NOUVEAU : Modale d'import PDF Cantine -->
|
||||||
|
<div id="importMenuModal" class="pf-modal">
|
||||||
|
<div class="pf-modal-content">
|
||||||
|
<div class="pf-modal-header" style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-light); padding-bottom: 15px; margin-bottom: 20px;">
|
||||||
|
<h3 class="pf-modal-title" style="margin: 0; border: none; padding: 0;">📄 <?= tr('meal_import_title') ?></h3>
|
||||||
|
<button type="button" class="pf-modal-close" style="background: none; border: none; font-size: 1.5rem; cursor: pointer; color: var(--text-muted);" onclick="document.getElementById('importMenuModal').classList.remove('open'); document.body.classList.remove('no-scroll');">×</button>
|
||||||
|
</div>
|
||||||
|
<form id="importMenuForm" onsubmit="importPdfMenu(event)" action="/modules/food/includes/api/import-pdf.php" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
||||||
|
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label"><?= tr('meal_import_child') ?></label>
|
||||||
|
<select name="person_id" class="pf-input" required>
|
||||||
|
<option value="" disabled selected>-- <?= tr('meal_select_child') ?> --</option>
|
||||||
|
<?php foreach ($members as $m): ?>
|
||||||
|
<option value="<?= $m['id'] ?>"><?= htmlspecialchars($m['name']) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label"><?= tr('meal_pdf_file') ?></label>
|
||||||
|
<div class="meal-dropzone" onclick="document.getElementById('canteenPdf').click()">
|
||||||
|
<span class="meal-dropzone-icon">📥</span>
|
||||||
|
<span id="pdfFileName"><?= tr('meal_drop_pdf') ?></span>
|
||||||
|
<input type="file" id="canteenPdf" name="pdf_file" accept=".pdf" style="display: none;" required onchange="document.getElementById('pdfFileName').innerText = this.files[0] ? this.files[0].name : '<?= addslashes(tr('meal_drop_pdf')) ?>';">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="pf-btn btn-secondary" onclick="document.getElementById('importMenuModal').classList.remove('open'); document.body.classList.remove('no-scroll');"><?= tr('btn_cancel') ?></button>
|
||||||
|
<button type="submit" class="pf-btn" id="btnImportPdf"><?= tr('btn_import') ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Sauvegarde automatique AJAX à chaque modification
|
||||||
|
document.querySelectorAll('.js-meal-input').forEach(input => {
|
||||||
|
input.addEventListener('change', async function() {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', 'save_cell');
|
||||||
|
fd.append('plan_date', this.dataset.d);
|
||||||
|
fd.append('service', this.dataset.s);
|
||||||
|
fd.append('person_id', this.dataset.p);
|
||||||
|
fd.append('meal_name', this.value);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pachaFetch('/modules/food/includes/api/save-meals.php', { method: 'POST', body: fd });
|
||||||
|
flashSuccess(this);
|
||||||
|
} catch(e) {
|
||||||
|
console.error("Save error", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Focus Context Bar
|
||||||
|
let copyState = { type: null, sourceDate: null, service: null, personIdx: null, sourceElements: [], fullDayName: '' };
|
||||||
|
|
||||||
|
document.addEventListener('focusin', (e) => {
|
||||||
|
if(e.target.classList.contains('js-meal-input')) {
|
||||||
|
copyState = {
|
||||||
|
type: 'cell',
|
||||||
|
sourceDate: e.target.getAttribute('data-d'),
|
||||||
|
service: e.target.getAttribute('data-s'),
|
||||||
|
personIdx: e.target.getAttribute('data-p'),
|
||||||
|
sourceElements: [e.target],
|
||||||
|
fullDayName: ''
|
||||||
|
};
|
||||||
|
document.getElementById('contextToolbar').classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('focusout', (e) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if(document.activeElement && !document.activeElement.classList.contains('js-meal-input')) {
|
||||||
|
document.getElementById('contextToolbar').classList.remove('active');
|
||||||
|
}
|
||||||
|
}, 150);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Actions Toolbar
|
||||||
|
window.applyToAllInRow = async function() {
|
||||||
|
if(copyState.type !== 'cell' || !copyState.sourceElements[0].value.trim()) return;
|
||||||
|
const val = copyState.sourceElements[0].value;
|
||||||
|
const inputs = document.querySelectorAll(`.js-meal-input[data-d="${copyState.sourceDate}"][data-s="${copyState.service}"]`);
|
||||||
|
|
||||||
|
let updates = [];
|
||||||
|
inputs.forEach(inp => {
|
||||||
|
if(inp !== copyState.sourceElements[0]) {
|
||||||
|
inp.value = val;
|
||||||
|
flashSuccess(inp);
|
||||||
|
updates.push({ plan_date: inp.dataset.d, service: inp.dataset.s, person_id: inp.dataset.p, meal_name: val });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updates.length > 0) {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', 'save_bulk');
|
||||||
|
fd.append('updates', JSON.stringify(updates));
|
||||||
|
await pachaFetch('/modules/food/includes/api/save-meals.php', { method: 'POST', body: fd });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.openCopySheetFromCell = function() {
|
||||||
|
if(!copyState.sourceElements[0] || !copyState.sourceElements[0].value.trim()) return;
|
||||||
|
if(document.activeElement) document.activeElement.blur();
|
||||||
|
|
||||||
|
const svcLabel = copyState.service === 'lunch' ? (window.I18N['meal_lunch'] || 'Midi') : (window.I18N['meal_dinner'] || 'Soir');
|
||||||
|
const dObj = new Date(copyState.sourceDate);
|
||||||
|
const dayKey = dObj.toLocaleDateString('en-US', {weekday: 'short'}).toLowerCase();
|
||||||
|
const dayName = window.I18N['day_full_' + dayKey] || dObj.toLocaleDateString();
|
||||||
|
|
||||||
|
document.getElementById('copySourceLabel').innerText = `Source : ${window.I18N['source_individual'] || 'Individuel'} (${dayName} ${svcLabel})`;
|
||||||
|
|
||||||
|
document.getElementById('copyModal').classList.add('open');
|
||||||
|
document.body.classList.add('no-scroll');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.openCopySheetFromRow = function(dateStr, service, fullDayName) {
|
||||||
|
const inputs = document.querySelectorAll(`.js-meal-input[data-d="${dateStr}"][data-s="${service}"]`);
|
||||||
|
if (!Array.from(inputs).some(inp => inp.value.trim() !== '')) return;
|
||||||
|
|
||||||
|
copyState = { type: 'row', sourceDate: dateStr, service: service, sourceElements: Array.from(inputs), fullDayName: fullDayName };
|
||||||
|
const svcLabel = service === 'lunch' ? (window.I18N['meal_lunch'] || 'Midi') : (window.I18N['meal_dinner'] || 'Soir');
|
||||||
|
document.getElementById('copySourceLabel').innerText = `Source : ${window.I18N['source_row'] || 'Repas commun'} (${fullDayName} ${svcLabel})`;
|
||||||
|
|
||||||
|
document.getElementById('copyModal').classList.add('open');
|
||||||
|
document.body.classList.add('no-scroll');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.closeCopyModal = function() {
|
||||||
|
document.getElementById('copyModal').classList.remove('open');
|
||||||
|
document.body.classList.remove('no-scroll');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.executeCopy = async function(targetDate, targetService) {
|
||||||
|
let updates = [];
|
||||||
|
|
||||||
|
if(copyState.type === 'cell') {
|
||||||
|
const val = copyState.sourceElements[0].value;
|
||||||
|
const targetInput = document.querySelector(`.js-meal-input[data-d="${targetDate}"][data-s="${targetService}"][data-p="${copyState.personIdx}"]`);
|
||||||
|
if(targetInput) {
|
||||||
|
targetInput.value = val;
|
||||||
|
flashSuccess(targetInput);
|
||||||
|
updates.push({ plan_date: targetDate, service: targetService, person_id: copyState.personIdx, meal_name: val });
|
||||||
|
}
|
||||||
|
} else if (copyState.type === 'row') {
|
||||||
|
const targetInputs = document.querySelectorAll(`.js-meal-input[data-d="${targetDate}"][data-s="${targetService}"]`);
|
||||||
|
copyState.sourceElements.forEach((inp, idx) => {
|
||||||
|
if(inp.value.trim() !== '') {
|
||||||
|
targetInputs[idx].value = inp.value;
|
||||||
|
flashSuccess(targetInputs[idx]);
|
||||||
|
updates.push({ plan_date: targetDate, service: targetService, person_id: targetInputs[idx].dataset.p, meal_name: inp.value });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
closeCopyModal();
|
||||||
|
|
||||||
|
if (updates.length > 0) {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', 'save_bulk');
|
||||||
|
fd.append('updates', JSON.stringify(updates));
|
||||||
|
await pachaFetch('/modules/meals/includes/api/save-meals.php', { method: 'POST', body: fd });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function flashSuccess(input) {
|
||||||
|
input.style.transition = 'background 0.3s';
|
||||||
|
input.style.background = 'var(--alert-success-bg)';
|
||||||
|
setTimeout(() => input.style.background = 'transparent', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOUVEAU : Fonction Javascript d'Import PDF
|
||||||
|
window.importPdfMenu = async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = e.target;
|
||||||
|
const btn = document.getElementById('btnImportPdf');
|
||||||
|
const oldText = btn.innerText;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerText = '⏳...';
|
||||||
|
|
||||||
|
const fd = new FormData(form);
|
||||||
|
const actionUrl = form.getAttribute('action');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await pachaFetch(actionUrl, { method: 'POST', body: fd });
|
||||||
|
if (res && res.success) {
|
||||||
|
document.getElementById('importMenuModal').classList.remove('open');
|
||||||
|
document.body.classList.remove('no-scroll');
|
||||||
|
if (window.showToast) showToast(window.I18N['meal_import_success'] || 'Menus importés !', 'success');
|
||||||
|
setTimeout(() => window.location.reload(), 1000);
|
||||||
|
} else {
|
||||||
|
if (window.showToast) showToast(res.error || window.I18N['error_occured'], 'error');
|
||||||
|
else alert(res.error);
|
||||||
|
}
|
||||||
|
} catch(err) {
|
||||||
|
if (window.showToast) showToast(window.I18N['error_occured'], 'error');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerText = oldText;
|
||||||
|
form.reset();
|
||||||
|
document.getElementById('pdfFileName').innerText = window.I18N['meal_drop_pdf'] || 'Cliquer pour ajouter le PDF';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
+64
-7
@@ -108,15 +108,38 @@ if ($action === 'vehicles') {
|
|||||||
gOk($stmt->fetchAll());
|
gOk($stmt->fetchAll());
|
||||||
}
|
}
|
||||||
if ($method === 'POST') {
|
if ($method === 'POST') {
|
||||||
$photo = handleUpload('photo', $UPLOAD_DIR); $d = $_POST ?: gBody();
|
$photo = handleUpload('photo', $UPLOAD_DIR);
|
||||||
$stmt = $pdo->prepare("INSERT INTO pf_vehicles (name,brand,model,year,license_plate,vin,fuel_type,color,purchase_date,purchase_price,current_km,photo,notes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)");
|
$d = $_POST ?: gBody();
|
||||||
$stmt->execute([$d['name']??'', $d['brand']??'', $d['model']??'', $d['year']??null, $d['license_plate']??null, $d['vin']??null, $d['fuel_type']??'Essence', $d['color']??null, $d['purchase_date']??null, $d['purchase_price']??null, $d['current_km']??0, $photo, $d['notes']??null]);
|
|
||||||
|
// 🔥 LE FIX EST ICI : Fonctions boucliers pour MySQL Strict Mode
|
||||||
|
$nullInt = fn($v) => ($v === '' || $v === null) ? null : (int)$v;
|
||||||
|
$nullFloat = fn($v) => ($v === '' || $v === null) ? null : (float)$v;
|
||||||
|
$nullStr = fn($v) => ($v === '' || $v === null) ? null : $v;
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pf_vehicles (name,brand,model,year,license_plate,vin,fuel_type,consumption,color,purchase_date,purchase_price,current_km,photo,notes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
|
||||||
|
$stmt->execute([
|
||||||
|
$d['name'] ?? '',
|
||||||
|
$d['brand'] ?? '',
|
||||||
|
$d['model'] ?? '',
|
||||||
|
$nullInt($d['year'] ?? null),
|
||||||
|
$nullStr($d['license_plate'] ?? null),
|
||||||
|
$nullStr($d['vin'] ?? null),
|
||||||
|
$d['fuel_type'] ?? 'Essence',
|
||||||
|
$nullFloat($d['consumption'] ?? null),
|
||||||
|
$nullStr($d['color'] ?? null),
|
||||||
|
$nullStr($d['purchase_date'] ?? null),
|
||||||
|
$nullFloat($d['purchase_price'] ?? null),
|
||||||
|
$nullInt($d['current_km'] ?? null) ?? 0,
|
||||||
|
$photo,
|
||||||
|
$nullStr($d['notes'] ?? null)
|
||||||
|
]);
|
||||||
gOk(['id' => $pdo->lastInsertId()]);
|
gOk(['id' => $pdo->lastInsertId()]);
|
||||||
}
|
}
|
||||||
if ($method === 'PUT') {
|
if ($method === 'PUT') {
|
||||||
$id = $_GET['id'] ?? null; if (!$id) gErr('ID manquant'); $d = gBody();
|
$id = $_GET['id'] ?? null; if (!$id) gErr('ID manquant'); $d = gBody();
|
||||||
$int_fields = ['year','current_km']; $float_fields = ['purchase_price'];
|
$int_fields = ['year','current_km'];
|
||||||
$fields = ['name','brand','model','year','license_plate','vin','fuel_type','color','purchase_date','purchase_price','current_km','notes'];
|
$float_fields = ['purchase_price', 'consumption'];
|
||||||
|
$fields = ['name','brand','model','year','license_plate','vin','fuel_type','consumption','color','purchase_date','purchase_price','current_km','notes'];
|
||||||
$sets = array_map(fn($f) => "$f = ?", $fields);
|
$sets = array_map(fn($f) => "$f = ?", $fields);
|
||||||
$vals = array_map(function($f) use ($d, $int_fields, $float_fields) {
|
$vals = array_map(function($f) use ($d, $int_fields, $float_fields) {
|
||||||
$v = $d[$f] ?? null;
|
$v = $d[$f] ?? null;
|
||||||
@@ -259,7 +282,23 @@ if ($action === 'maintenances') {
|
|||||||
$photo = handleUpload('invoice_photo', $UPLOAD_DIR); $d = $_POST ?: gBody();
|
$photo = handleUpload('invoice_photo', $UPLOAD_DIR); $d = $_POST ?: gBody();
|
||||||
if (!($d['vehicle_id'] ?? null)) gErr('vehicle_id manquant');
|
if (!($d['vehicle_id'] ?? null)) gErr('vehicle_id manquant');
|
||||||
$stmt = $pdo->prepare("INSERT INTO pf_maintenances (vehicle_id,type,description,date,km,cost,mechanic,garage_name,next_km,next_date,invoice_photo,notes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)");
|
$stmt = $pdo->prepare("INSERT INTO pf_maintenances (vehicle_id,type,description,date,km,cost,mechanic,garage_name,next_km,next_date,invoice_photo,notes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)");
|
||||||
$stmt->execute([$d['vehicle_id'], $d['type']??'', $d['description']??null, $d['date']??date('Y-m-d'), $d['km']??null, $d['cost']??0, $d['mechanic']??null, $d['garage_name']??null, $d['next_km']??null, $d['next_date']??null, $photo, $d['notes']??null]);
|
$nullInt = fn($v) => ($v === '' || $v === null) ? null : (int)$v;
|
||||||
|
$nullFloat = fn($v) => ($v === '' || $v === null) ? null : (float)$v;
|
||||||
|
$nullStr = fn($v) => ($v === '' || $v === null) ? null : $v;
|
||||||
|
$stmt->execute([
|
||||||
|
$d['vehicle_id'],
|
||||||
|
$d['type'] ?? '',
|
||||||
|
$nullStr($d['description'] ?? null),
|
||||||
|
$d['date'] ?? date('Y-m-d'),
|
||||||
|
$nullInt($d['km'] ?? null),
|
||||||
|
$nullFloat($d['cost'] ?? null),
|
||||||
|
$nullStr($d['mechanic'] ?? null),
|
||||||
|
$nullStr($d['garage_name'] ?? null),
|
||||||
|
$nullInt($d['next_km'] ?? null),
|
||||||
|
$nullStr($d['next_date'] ?? null),
|
||||||
|
$photo,
|
||||||
|
$nullStr($d['notes'] ?? null),
|
||||||
|
]);
|
||||||
$mid = $pdo->lastInsertId();
|
$mid = $pdo->lastInsertId();
|
||||||
if (!empty($d['km'])) { $pdo->prepare("UPDATE pf_vehicles SET current_km = GREATEST(current_km, ?), updated_at = NOW() WHERE id = ?")->execute([$d['km'], $d['vehicle_id']]); }
|
if (!empty($d['km'])) { $pdo->prepare("UPDATE pf_vehicles SET current_km = GREATEST(current_km, ?), updated_at = NOW() WHERE id = ?")->execute([$d['km'], $d['vehicle_id']]); }
|
||||||
gOk(['id' => $mid]);
|
gOk(['id' => $mid]);
|
||||||
@@ -307,8 +346,26 @@ if ($action === 'parts') {
|
|||||||
}
|
}
|
||||||
if ($method === 'POST') {
|
if ($method === 'POST') {
|
||||||
$photo = handleUpload('photo', $UPLOAD_DIR); $d = $_POST ?: gBody();
|
$photo = handleUpload('photo', $UPLOAD_DIR); $d = $_POST ?: gBody();
|
||||||
|
$nullInt = fn($v) => ($v === '' || $v === null) ? null : (int)$v;
|
||||||
|
$nullFloat = fn($v) => ($v === '' || $v === null) ? null : (float)$v;
|
||||||
|
$nullStr = fn($v) => ($v === '' || $v === null) ? null : $v;
|
||||||
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO pf_parts (vehicle_id,maintenance_id,brand,reference,name,category,price,quantity,unit,supplier,purchase_date,photo,notes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)");
|
$stmt = $pdo->prepare("INSERT INTO pf_parts (vehicle_id,maintenance_id,brand,reference,name,category,price,quantity,unit,supplier,purchase_date,photo,notes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)");
|
||||||
$stmt->execute([$d['vehicle_id']??null, $d['maintenance_id']??null, $d['brand']??null, $d['reference']??null, $d['name']??'', $d['category']??'Autre', $d['price']??0, $d['quantity']??1, $d['unit']??'pièce', $d['supplier']??null, $d['purchase_date']??null, $photo, $d['notes']??null]);
|
$stmt->execute([
|
||||||
|
$nullInt($d['vehicle_id'] ?? null),
|
||||||
|
$nullInt($d['maintenance_id'] ?? null),
|
||||||
|
$nullStr($d['brand'] ?? null),
|
||||||
|
$nullStr($d['reference'] ?? null),
|
||||||
|
$d['name'] ?? '',
|
||||||
|
$d['category'] ?? 'Autre',
|
||||||
|
$nullFloat($d['price'] ?? null) ?? 0,
|
||||||
|
$nullInt($d['quantity'] ?? null) ?? 1,
|
||||||
|
$d['unit'] ?? 'pièce',
|
||||||
|
$nullStr($d['supplier'] ?? null),
|
||||||
|
$nullStr($d['purchase_date'] ?? null),
|
||||||
|
$photo,
|
||||||
|
$nullStr($d['notes'] ?? null)
|
||||||
|
]);
|
||||||
gOk(['id' => $pdo->lastInsertId()]);
|
gOk(['id' => $pdo->lastInsertId()]);
|
||||||
}
|
}
|
||||||
if ($method === 'PUT') {
|
if ($method === 'PUT') {
|
||||||
|
|||||||
+1409
-643
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
require __DIR__ . '/../../includes/auth.php';
|
||||||
|
require __DIR__ . '/../../includes/db.php';
|
||||||
|
require_login();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$action = $_POST['action'] ?? $_GET['action'] ?? '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($action === 'get_all') {
|
||||||
|
$occ = $pdo->query("SELECT * FROM pf_gift_occasions ORDER BY sort_order ASC, id ASC")->fetchAll();
|
||||||
|
$proches = $pdo->query("SELECT * FROM pf_people WHERE role IN ('proche_adulte', 'proche_enfant') ORDER BY name ASC")->fetchAll();
|
||||||
|
echo json_encode(['success' => true, 'data' => ['occasions' => $occ, 'proches' => $proches]]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'save_occasion') {
|
||||||
|
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
|
||||||
|
$name = trim($_POST['name']);
|
||||||
|
$icon = trim($_POST['icon']) ?: '🎁';
|
||||||
|
$month_date = trim($_POST['month_date']) ?: null;
|
||||||
|
|
||||||
|
if (empty($name)) throw new Exception("Le nom est obligatoire.");
|
||||||
|
|
||||||
|
if ($id) {
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_gift_occasions SET name=?, icon=?, month_date=? WHERE id=?");
|
||||||
|
$stmt->execute([$name, $icon, $month_date, $id]);
|
||||||
|
} else {
|
||||||
|
$code = 'CUST_' . strtoupper(substr(preg_replace('/[^A-Za-z0-9]/', '', $name), 0, 4)) . '_' . rand(100, 999);
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pf_gift_occasions (code, name, icon, month_date) VALUES (?, ?, ?, ?)");
|
||||||
|
$stmt->execute([$code, $name, $icon, $month_date]);
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'delete_occasion') {
|
||||||
|
$id = (int)$_POST['id'];
|
||||||
|
$pdo->prepare("DELETE FROM pf_gift_occasions WHERE id=?")->execute([$id]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'toggle_occasion') {
|
||||||
|
$id = (int)$_POST['id'];
|
||||||
|
$state = (int)$_POST['state'];
|
||||||
|
$pdo->prepare("UPDATE pf_gift_occasions SET is_active=? WHERE id=?")->execute([$state, $id]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'save_proche') {
|
||||||
|
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
|
||||||
|
$name = trim($_POST['name']);
|
||||||
|
$role = $_POST['role'];
|
||||||
|
|
||||||
|
if (empty($name)) throw new Exception("Le nom est obligatoire.");
|
||||||
|
if (!in_array($role, ['proche_adulte', 'proche_enfant'])) throw new Exception("Rôle invalide.");
|
||||||
|
|
||||||
|
if ($id) {
|
||||||
|
// Sécurité : on s'assure qu'on modifie bien un proche et pas un parent
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_people SET name=?, role=? WHERE id=? AND role IN ('proche_adulte', 'proche_enfant')");
|
||||||
|
$stmt->execute([$name, $role, $id]);
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pf_people (name, role, color) VALUES (?, ?, '#94a3b8')");
|
||||||
|
$stmt->execute([$name, $role]);
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'delete_proche') {
|
||||||
|
$id = (int)$_POST['id'];
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM pf_people WHERE id=? AND role IN ('proche_adulte', 'proche_enfant')");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
+243
-91
@@ -6,7 +6,6 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cl-titlebar {
|
.cl-titlebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -17,7 +16,6 @@
|
|||||||
border-bottom: 1px solid var(--border-light);
|
border-bottom: 1px solid var(--border-light);
|
||||||
padding-bottom: 15px;
|
padding-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cl-view-switch {
|
.cl-view-switch {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
@@ -25,7 +23,6 @@
|
|||||||
border: 1px solid var(--border-light);
|
border: 1px solid var(--border-light);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cl-view-btn {
|
.cl-view-btn {
|
||||||
padding: 6px 16px;
|
padding: 6px 16px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -35,13 +32,12 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
transition: 0.2s;
|
transition: 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cl-view-btn.is-active {
|
.cl-view-btn.is-active {
|
||||||
background: var(--text-main);
|
background: var(--text-main);
|
||||||
color: var(--bg-panel);
|
color: var(--bg-panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === FILTRES STICKY === */
|
/* === FILTRES === */
|
||||||
.pf-filter-bar {
|
.pf-filter-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -52,7 +48,6 @@
|
|||||||
z-index: 50;
|
z-index: 50;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pf-filter-label {
|
.pf-filter-label {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -62,36 +57,6 @@
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pf-filter-select {
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 20px; /* Forme de pillule */
|
|
||||||
border: 1px solid var(--border-light);
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-main);
|
|
||||||
outline: none;
|
|
||||||
font-family: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
width: auto; /* Empêche de prendre toute la largeur */
|
|
||||||
appearance: none; /* Nettoie la flèche système... */
|
|
||||||
-webkit-appearance: none;
|
|
||||||
-moz-appearance: none;
|
|
||||||
/* ...pour mettre une flèche personnalisée plus discrète */
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2364748b'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: right 10px center;
|
|
||||||
background-size: 12px;
|
|
||||||
padding-right: 28px; /* Place pour la flèche */
|
|
||||||
}
|
|
||||||
|
|
||||||
.pf-filter-select:focus {
|
|
||||||
border-color: var(--primary);
|
|
||||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === SECTIONS ENFANTS === */
|
/* === SECTIONS ENFANTS === */
|
||||||
.pf-child-section {
|
.pf-child-section {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
@@ -121,10 +86,10 @@
|
|||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
border-bottom: 1px solid var(--border-light);
|
border-bottom: 1px solid var(--border-light);
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
.pf-child-totals-bar::-webkit-scrollbar {
|
.pf-child-totals-bar::-webkit-scrollbar {
|
||||||
display: none; /* Chrome/Safari */
|
display: none;
|
||||||
}
|
}
|
||||||
.pf-summary-pill {
|
.pf-summary-pill {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -182,12 +147,6 @@
|
|||||||
color: var(--success);
|
color: var(--success);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
.pf-gift-badges-col {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.pf-pill-adult {
|
.pf-pill-adult {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -196,14 +155,6 @@
|
|||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
.pf-pill-occ {
|
|
||||||
font-size: 0.65rem;
|
|
||||||
font-weight: 700;
|
|
||||||
background: #fef3c7;
|
|
||||||
color: #b45309;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.pf-gift-payer {
|
.pf-gift-payer {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
@@ -241,53 +192,76 @@
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
border-right: 2px solid var(--border-light);
|
border-right: 2px solid var(--border-light);
|
||||||
}
|
}
|
||||||
|
.gift-tricount-section {
|
||||||
/* Icônes des fêtes (Tió, Noël, etc.) plus petites et bien alignées */
|
border: 1px solid var(--border-light);
|
||||||
.cl-occasion-icon {
|
margin-top: 40px;
|
||||||
width: 20px; /* Réduit pour être discret */
|
}
|
||||||
height: 20px;
|
.gift-tricount-title {
|
||||||
object-fit: contain;
|
margin-top: 0;
|
||||||
vertical-align: middle;
|
color: var(--text-main);
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
.gift-tricount-success {
|
||||||
|
color: var(--success);
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.gift-tricount-debt-amount {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
.gift-matrix-details {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
.gift-matrix-summary {
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.gift-matrix-wrapper {
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.cl-debt-matrix th.sticky-col.bg-header {
|
||||||
|
background: var(--bg-page);
|
||||||
|
}
|
||||||
|
.cl-debt-matrix th.sticky-col.bg-body {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
}
|
||||||
|
.gift-cell-danger {
|
||||||
|
color: var(--danger);
|
||||||
|
font-weight: 700;
|
||||||
|
background: var(--bg-danger-soft);
|
||||||
|
}
|
||||||
|
.gift-cell-muted {
|
||||||
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === TYPOGRAPHIE & ICONES === */
|
||||||
|
.cl-occasion-icon {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
.cl-occasion-title {
|
.cl-occasion-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px; /* Espace réduit entre l'icône et le texte */
|
}
|
||||||
|
.gift-empty-state {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-style: italic;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.pf-child-section {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
.pf-filter-bar {
|
|
||||||
padding: 0 5px;
|
|
||||||
}
|
|
||||||
.pf-filter-label {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.pf-gift-feed {
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
.pf-gift-title {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
.pf-gift-price {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 400px) {
|
|
||||||
.pf-gift-feed {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* === COMPOSANT MULTI-SELECT VANILLA === */
|
/* === COMPOSANT MULTI-SELECT VANILLA === */
|
||||||
.pf-multi-select {
|
.pf-multi-select {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Le bouton déclencheur (réutilise le style des filtres) */
|
|
||||||
.pf-ms-trigger {
|
.pf-ms-trigger {
|
||||||
padding: 6px 16px;
|
padding: 6px 16px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
@@ -310,8 +284,6 @@
|
|||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
|
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Le menu déroulant */
|
|
||||||
.pf-ms-dropdown {
|
.pf-ms-dropdown {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -333,8 +305,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
animation: popDown 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
animation: popDown 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Les options (checkboxes) */
|
|
||||||
.pf-ms-option {
|
.pf-ms-option {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -375,3 +345,185 @@
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.pf-child-section {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
.pf-filter-bar {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
.pf-filter-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.pf-gift-feed {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
.pf-gift-title {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.pf-gift-price {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.pf-gift-feed {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === MODALE EDITION CADEAU === */
|
||||||
|
.gift-modal-custom-content {
|
||||||
|
max-width: 500px;
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
|
.gift-modal-custom-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.gift-modal-custom-title {
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.gift-modal-close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.gift-form-group-spaced {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
.gift-modal-custom-footer {
|
||||||
|
padding-top: 15px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
border-top: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gl-pane {
|
||||||
|
display: none;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
.gl-pane.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.gl-pane-title {
|
||||||
|
margin-top: 0;
|
||||||
|
color: var(--text-main);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.gl-pane-desc {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gl-list-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.gl-form-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-end;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.gl-divider {
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
.gl-input-sm {
|
||||||
|
flex: 0 0 60px;
|
||||||
|
}
|
||||||
|
.gl-input-md {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-loader {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.align-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================
|
||||||
|
COMPOSANTS MODALE & EDITION IN-PLACE
|
||||||
|
========================================= */
|
||||||
|
|
||||||
|
/* Sélecteur d'émojis horizontal */
|
||||||
|
.emoji-picker {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.emoji-option {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
background: transparent;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.emoji-option:hover {
|
||||||
|
background: var(--bg-page);
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
.emoji-option.selected {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: var(
|
||||||
|
--bg-subtle
|
||||||
|
); /* ou un bleu très clair rgba(59, 130, 246, 0.1) */
|
||||||
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alignement des boutons d'ajout */
|
||||||
|
.modal-actions-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Édition In-Place sur les lignes de la liste */
|
||||||
|
.inline-edit-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 100%;
|
||||||
|
flex-wrap: wrap; /* Évite que ça dépasse sur mobile */
|
||||||
|
}
|
||||||
|
.inline-edit-form .pf-input {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
+1033
-878
File diff suppressed because it is too large.
Load diff
+1681
-366
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
// modules/holidays/includes/api/delete_attachment.php
|
||||||
|
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||||
|
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||||
|
require_login();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Méthode non autorisée']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file_id = isset($_POST['file_id']) ? (int)$_POST['file_id'] : 0;
|
||||||
|
$holiday_id = isset($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : 0;
|
||||||
|
|
||||||
|
if ($file_id === 0 || $holiday_id === 0) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Paramètres manquants']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. On récupère le chemin exact du fichier pour le supprimer du NAS
|
||||||
|
$stmt = $pdo->prepare("SELECT file_path FROM pf_holidays_attachments WHERE id = ? AND holiday_id = ?");
|
||||||
|
$stmt->execute([$file_id, $holiday_id]);
|
||||||
|
$file = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($file) {
|
||||||
|
$absolutePath = dirname(__DIR__, 4) . '/' . $file['file_path'];
|
||||||
|
|
||||||
|
// 2. On supprime physiquement le fichier s'il existe
|
||||||
|
if (file_exists($absolutePath)) {
|
||||||
|
unlink($absolutePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. On nettoie la base de données
|
||||||
|
$del = $pdo->prepare("DELETE FROM pf_holidays_attachments WHERE id = ?");
|
||||||
|
$del->execute([$file_id]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Fichier introuvable ou non autorisé']);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
// modules/holidays/includes/api/get_attachments.php
|
||||||
|
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||||
|
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||||
|
require_login();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$holiday_id = isset($_GET['holiday_id']) ? (int)$_GET['holiday_id'] : 0;
|
||||||
|
$item_id = isset($_GET['item_id']) ? (int)$_GET['item_id'] : 0;
|
||||||
|
|
||||||
|
if ($holiday_id === 0) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'ID manquant']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On récupère les documents de cette étape spécifique
|
||||||
|
$stmt = $pdo->prepare("SELECT id, file_name, file_path FROM pf_holidays_attachments WHERE holiday_id = ? AND item_id = ? ORDER BY uploaded_at DESC");
|
||||||
|
$stmt->execute([$holiday_id, $item_id]);
|
||||||
|
$files = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'files' => $files]);
|
||||||
@@ -4,32 +4,117 @@ require dirname(__DIR__, 4) . '/includes/auth.php';
|
|||||||
require dirname(__DIR__, 4) . '/includes/db.php';
|
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||||
require_login();
|
require_login();
|
||||||
|
|
||||||
// INTERCEPTION AJAX : Sauvegarde du planning (Drag & Drop / Durée)
|
// ---------------------------------------------------------------------------
|
||||||
if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime', 'update_item_duration'])) {
|
// INTERCEPTIONS AJAX (Exécution ultra rapide sans rechargement lourd)
|
||||||
$itemId = (int)$_POST['item_id'];
|
// ---------------------------------------------------------------------------
|
||||||
|
if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime', 'update_item_duration', 'add_single_item', 'update_holiday_note'])) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
session_write_close();
|
||||||
|
|
||||||
if ($_POST['action'] === 'update_item_datetime') {
|
try {
|
||||||
$itemDate = !empty($_POST['item_date']) ? $_POST['item_date'] : null;
|
if ($_POST['action'] === 'update_holiday_note') {
|
||||||
$itemTime = !empty($_POST['item_time']) ? $_POST['item_time'] : null;
|
$hId = (int)$_POST['holiday_id'];
|
||||||
$stmt = $pdo->prepare("UPDATE pf_holidays_items SET item_date = ?, item_time = ? WHERE id = ?");
|
$notes = $_POST['notes'] ?? '';
|
||||||
$stmt->execute([$itemDate, $itemTime, $itemId]);
|
$stmt = $pdo->prepare("UPDATE pf_holidays SET notes = ? WHERE id = ?");
|
||||||
} else {
|
$stmt->execute([$notes, $hId]);
|
||||||
$duration = (int)$_POST['duration'];
|
echo json_encode(['success' => true]);
|
||||||
$stmt = $pdo->prepare("UPDATE pf_holidays_items SET duration = ? WHERE id = ?");
|
exit;
|
||||||
$stmt->execute([$duration, $itemId]);
|
}
|
||||||
|
|
||||||
|
if ($_POST['action'] === 'add_single_item') {
|
||||||
|
$holiday_id = (int)$_POST['holiday_id'];
|
||||||
|
$sort_order = (int)$_POST['sort_order'];
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT location_name, lat, lng, step_start_date, step_end_date, step_type FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ? LIMIT 1");
|
||||||
|
$stmt->execute([$holiday_id, $sort_order]);
|
||||||
|
$stepInfo = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($stepInfo) {
|
||||||
|
$dur = isset($_POST['duration']) ? (int)$_POST['duration'] : 1;
|
||||||
|
$date = !empty($_POST['item_date']) ? $_POST['item_date'] : null;
|
||||||
|
$time = !empty($_POST['item_time']) ? $_POST['item_time'] : null;
|
||||||
|
|
||||||
|
// Récupération sécurisée du contexte
|
||||||
|
$context = !empty($_POST['expense_context']) ? $_POST['expense_context'] : (!empty($_POST['context']) ? $_POST['context'] : 'local');
|
||||||
|
$amount = (float)$_POST['amount'];
|
||||||
|
$name = $_POST['name'] ?? 'Trajet';
|
||||||
|
$category = $_POST['category'] ?? 'transport';
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
// 🛡️ ANTI-DOUBLON ABSOLU
|
||||||
|
if ($context === 'transit') {
|
||||||
|
$stmtCheck = $pdo->prepare("SELECT id FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ? AND expense_context = 'transit'");
|
||||||
|
$stmtCheck->execute([$holiday_id, $sort_order]);
|
||||||
|
$existingId = $stmtCheck->fetchColumn();
|
||||||
|
|
||||||
|
if ($existingId) {
|
||||||
|
// Mise à jour de la ligne existante
|
||||||
|
$stmtUp = $pdo->prepare("UPDATE pf_holidays_items SET item_date = ?, item_time = ?, amount = ?, duration = ?, name = ? WHERE id = ?");
|
||||||
|
$stmtUp->execute([$date, $time, $amount, $dur, $name, $existingId]);
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode(['success' => true, 'id' => $existingId]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insertion si aucun doublon n'existe
|
||||||
|
$ins = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid, location_name, lat, lng, sort_order, step_start_date, step_end_date, step_type, expense_context, duration, item_date, item_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
|
$ins->execute([
|
||||||
|
$holiday_id, $category, $name, $amount, 0,
|
||||||
|
$stepInfo['location_name'], $stepInfo['lat'], $stepInfo['lng'],
|
||||||
|
$sort_order, $stepInfo['step_start_date'], $stepInfo['step_end_date'],
|
||||||
|
$stepInfo['step_type'], $context, $dur, $date, $time
|
||||||
|
]);
|
||||||
|
|
||||||
|
$newId = $pdo->lastInsertId();
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'id' => $newId]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Etape introuvable']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag & Drop planning : Date et Heure
|
||||||
|
if ($_POST['action'] === 'update_item_datetime') {
|
||||||
|
$itemId = (int)$_POST['item_id'];
|
||||||
|
$itemDate = !empty($_POST['item_date']) ? $_POST['item_date'] : null;
|
||||||
|
$itemTime = !empty($_POST['item_time']) ? $_POST['item_time'] : null;
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_holidays_items SET item_date = ?, item_time = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$itemDate, $itemTime, $itemId]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changement de durée (+/-)
|
||||||
|
if ($_POST['action'] === 'update_item_duration') {
|
||||||
|
$itemId = (int)$_POST['item_id'];
|
||||||
|
$duration = (int)$_POST['duration'];
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_holidays_items SET duration = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$duration, $itemId]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($pdo->inTransaction()) { $pdo->rollBack(); }
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode(['success' => true]);
|
|
||||||
exit; // Crucial : on arrête le script ici !
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------===========================================
|
||||||
|
// FORMULAIRE TRADITIONNEL (Soumission de la modale d'étape)
|
||||||
|
// --------------------------------===========================================
|
||||||
$holiday_id = (int)$_POST['holiday_id'];
|
$holiday_id = (int)$_POST['holiday_id'];
|
||||||
$location_name = trim($_POST['location_name']);
|
$location_name = trim($_POST['location_name']);
|
||||||
$lat = (float)$_POST['lat'];
|
$lat = (float)$_POST['lat'];
|
||||||
$lng = (float)$_POST['lng'];
|
$lng = (float)$_POST['lng'];
|
||||||
$old_sort_order = isset($_POST['old_sort_order']) && $_POST['old_sort_order'] !== '' ? (int)$_POST['old_sort_order'] : null;
|
$old_sort_order = isset($_POST['old_sort_order']) && $_POST['old_sort_order'] !== '' ? (int)$_POST['old_sort_order'] : null;
|
||||||
|
|
||||||
// 1. SUPPRESSION
|
// SUPPRESSION DE L'ÉTAPE
|
||||||
if (isset($_POST['action_delete']) && $_POST['action_delete'] === '1') {
|
if (isset($_POST['action_delete']) && $_POST['action_delete'] === '1') {
|
||||||
$pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ?")->execute([$holiday_id, $old_sort_order]);
|
$pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ?")->execute([$holiday_id, $old_sort_order]);
|
||||||
header("Location: /holidays.php?tab=holiday_detail&id=" . $holiday_id);
|
header("Location: /holidays.php?tab=holiday_detail&id=" . $holiday_id);
|
||||||
@@ -40,26 +125,34 @@ if ($holiday_id > 0 && !empty($location_name)) {
|
|||||||
try {
|
try {
|
||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
// 2. DÉTERMINER L'ORDRE (Identifiant de l'étape)
|
|
||||||
if ($old_sort_order !== null) {
|
if ($old_sort_order !== null) {
|
||||||
// Modification : on supprime l'ancien contenu de cette étape précise
|
|
||||||
$pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ?")->execute([$holiday_id, $old_sort_order]);
|
$pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ?")->execute([$holiday_id, $old_sort_order]);
|
||||||
$target_order = $old_sort_order;
|
$target_order = $old_sort_order;
|
||||||
} else {
|
} else {
|
||||||
// Ajout : on place à la fin (Max + 1)
|
// NOUVEAU : Logique d'intercalage d'étape
|
||||||
$stmtMax = $pdo->prepare("SELECT MAX(sort_order) FROM pf_holidays_items WHERE holiday_id = ?");
|
$insert_after = $_POST['insert_after'] ?? 'end';
|
||||||
$stmtMax->execute([$holiday_id]);
|
|
||||||
$max = $stmtMax->fetchColumn();
|
if ($insert_after === 'end') {
|
||||||
$target_order = ($max !== null) ? (int)$max + 1 : 0;
|
$stmtMax = $pdo->prepare("SELECT MAX(sort_order) FROM pf_holidays_items WHERE holiday_id = ?");
|
||||||
|
$stmtMax->execute([$holiday_id]);
|
||||||
|
$max = $stmtMax->fetchColumn();
|
||||||
|
$target_order = ($max !== null) ? (int)$max + 1 : 0;
|
||||||
|
} else {
|
||||||
|
$target_order = (int)$insert_after + 1;
|
||||||
|
// On décale toutes les étapes suivantes vers le bas
|
||||||
|
$pdo->prepare("UPDATE pf_holidays_items SET sort_order = sort_order + 1 WHERE holiday_id = ? AND sort_order >= ?")->execute([$holiday_id, $target_order]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupération des dates de l'étape globale
|
|
||||||
$step_start = !empty($_POST['step_start_date']) ? $_POST['step_start_date'] : null;
|
$step_start = !empty($_POST['step_start_date']) ? $_POST['step_start_date'] : null;
|
||||||
$step_end = !empty($_POST['step_end_date']) ? $_POST['step_end_date'] : null;
|
$step_end = !empty($_POST['step_end_date']) ? $_POST['step_end_date'] : null;
|
||||||
$is_return = isset($_POST['is_return']) ? 1 : 0; // NOUVEAU
|
$step_type = $_POST['step_type'] ?? 'stop';
|
||||||
|
|
||||||
// 3. INSERTION DES LIGNES (16 Colonnes)
|
if ($step_type === 'origin' || $step_type === 'destination') {
|
||||||
$stmt = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid, location_name, lat, lng, sort_order, notes, item_date, item_time, step_start_date, step_end_date, duration, is_return) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
$step_end = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid, location_name, lat, lng, sort_order, notes, item_date, item_time, step_start_date, step_end_date, duration, step_type, expense_context) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
$validItemsCount = 0;
|
$validItemsCount = 0;
|
||||||
|
|
||||||
if (isset($_POST['items']['name'])) {
|
if (isset($_POST['items']['name'])) {
|
||||||
@@ -75,19 +168,26 @@ if ($holiday_id > 0 && !empty($location_name)) {
|
|||||||
$date = !empty($_POST['items']['date'][$i]) ? $_POST['items']['date'][$i] : null;
|
$date = !empty($_POST['items']['date'][$i]) ? $_POST['items']['date'][$i] : null;
|
||||||
$time = !empty($_POST['items']['time'][$i]) ? $_POST['items']['time'][$i] : null;
|
$time = !empty($_POST['items']['time'][$i]) ? $_POST['items']['time'][$i] : null;
|
||||||
$dur = !empty($_POST['items']['duration'][$i]) ? (int)$_POST['items']['duration'][$i] : 1;
|
$dur = !empty($_POST['items']['duration'][$i]) ? (int)$_POST['items']['duration'][$i] : 1;
|
||||||
|
$context = !empty($_POST['items']['context'][$i]) ? $_POST['items']['context'][$i] : 'local';
|
||||||
|
|
||||||
// Ajout de $is_return à la fin
|
// Récupération des coordonnées spécifiques à l'activité (ou fallback sur l'étape)
|
||||||
$stmt->execute([$holiday_id, $cat, $name ?: tr('hdl_default_exp_name'), $amount, $paid, $location_name, $lat, $lng, $target_order, $note, $date, $time, $step_start, $step_end, $dur, $is_return]);
|
$itemLat = !empty($_POST['items']['lat'][$i]) ? (float)$_POST['items']['lat'][$i] : $lat;
|
||||||
|
$itemLng = !empty($_POST['items']['lng'][$i]) ? (float)$_POST['items']['lng'][$i] : $lng;
|
||||||
|
|
||||||
|
$stmt->execute([$holiday_id, $cat, $name, $amount, $paid, $location_name, $itemLat, $itemLng, $target_order, $note, $date, $time, $step_start, $step_end, $dur, $step_type, $context]);
|
||||||
$validItemsCount++;
|
$validItemsCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($validItemsCount === 0) {
|
if ($validItemsCount === 0) {
|
||||||
$stmt->execute([$holiday_id, 'activity', 'PF_TECHNICAL_POINT', 0, 1, $location_name, $lat, $lng, $target_order, '', null, null, $step_start, $step_end, 1, $is_return]);
|
$stmt->execute([$holiday_id, 'activity', 'PF_TECHNICAL_POINT', 0, 1, $location_name, $lat, $lng, $target_order, '', null, null, $step_start, $step_end, 1, $step_type, 'local']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_POST['set_as_return']) && $_POST['set_as_return'] == '1') {
|
||||||
|
$pdo->prepare("UPDATE pf_holidays SET return_step_id = ? WHERE id = ?")->execute([$target_order, $holiday_id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. GESTION DES FAVORIS
|
|
||||||
if (isset($_POST['save_favorite']) && $_POST['save_favorite'] == '1') {
|
if (isset($_POST['save_favorite']) && $_POST['save_favorite'] == '1') {
|
||||||
$stmtFav = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'holiday_favorites'");
|
$stmtFav = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'holiday_favorites'");
|
||||||
$favs = json_decode($stmtFav->fetchColumn() ?: '[]', true);
|
$favs = json_decode($stmtFav->fetchColumn() ?: '[]', true);
|
||||||
@@ -100,7 +200,10 @@ if ($holiday_id > 0 && !empty($location_name)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
} catch (Exception $e) { $pdo->rollBack(); die($e->getMessage()); }
|
} catch (Exception $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
die($e->getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
header("Location: /holidays.php?tab=holiday_detail&id=" . $holiday_id);
|
header("Location: /holidays.php?tab=holiday_detail&id=" . $holiday_id);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
// modules/holidays/includes/api/save_holiday.php
|
// modules/holidays/includes/api/save_holiday.php
|
||||||
|
|
||||||
// On remonte de 4 niveaux pour atteindre la racine (api -> includes -> holidays -> modules -> racine)
|
// On remonte de 4 niveaux pour atteindre la racine
|
||||||
require dirname(__DIR__, 4) . '/includes/auth.php';
|
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||||
require dirname(__DIR__, 4) . '/includes/db.php';
|
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||||
require_login(); // Si cette fonction nécessite une redirection, gère-la dans auth.php
|
require_login();
|
||||||
|
|
||||||
if (isset($_POST['action_delete']) && $_POST['action_delete'] == '1') {
|
if (isset($_POST['action_delete']) && $_POST['action_delete'] == '1') {
|
||||||
$stmt = $pdo->prepare("DELETE FROM pf_holidays WHERE id = ?");
|
$stmt = $pdo->prepare("DELETE FROM pf_holidays WHERE id = ?");
|
||||||
@@ -22,59 +22,58 @@ $status = $_POST['status'];
|
|||||||
$food = !empty($_POST['budget_food']) ? $_POST['budget_food'] : 0;
|
$food = !empty($_POST['budget_food']) ? $_POST['budget_food'] : 0;
|
||||||
$extra = !empty($_POST['budget_extra']) ? $_POST['budget_extra'] : 0;
|
$extra = !empty($_POST['budget_extra']) ? $_POST['budget_extra'] : 0;
|
||||||
$notes = $_POST['notes'];
|
$notes = $_POST['notes'];
|
||||||
|
// 🔥 NOUVEAU : On récupère le véhicule optionnel
|
||||||
|
$vehicle_id = !empty($_POST['vehicle_id']) ? (int)$_POST['vehicle_id'] : null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
// 1. Détection d'un changement de nom pour refaire l'appel API
|
||||||
|
$image_url = null;
|
||||||
|
$old_title = null;
|
||||||
if ($id) {
|
if ($id) {
|
||||||
// UPDATE
|
$stmtOld = $pdo->prepare("SELECT title, image_url FROM pf_holidays WHERE id = ?");
|
||||||
$sql = "UPDATE pf_holidays SET title=?, period_hint=?, start_date=?, end_date=?, status=?, budget_food=?, budget_extra=?, notes=? WHERE id=?";
|
$stmtOld->execute([$id]);
|
||||||
$stmt = $pdo->prepare($sql);
|
if ($row = $stmtOld->fetch()) {
|
||||||
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes, $id]);
|
$old_title = $row['title'];
|
||||||
} else {
|
$image_url = $row['image_url'];
|
||||||
// INSERT
|
}
|
||||||
$sql = "INSERT INTO pf_holidays (title, period_hint, start_date, end_date, status, budget_food, budget_extra, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes]);
|
|
||||||
$id = $pdo->lastInsertId();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GESTION INTELLIGENTE DES ITEMS
|
// 2. Appel à Pixabay si nouveau voyage ou si le titre a changé
|
||||||
if (!empty($_POST['items']['name'])) {
|
if (!$id || strcasecmp($old_title ?? '', $title) !== 0) {
|
||||||
$count = count($_POST['items']['name']);
|
$apiKey = '56931941-a86fbea4e14b88712cc1e9ed9';
|
||||||
// On prépare une requête qui met à jour si l'ID existe, sinon insère
|
$query = urlencode($title);
|
||||||
$stmtItem = $pdo->prepare("
|
// On force des photos horizontales HD
|
||||||
INSERT INTO pf_holidays_items (id, holiday_id, category, name, amount, is_paid, location_name)
|
$url = "https://pixabay.com/api/?key={$apiKey}&q={$query}&image_type=photo&orientation=horizontal&min_width=1280&per_page=3";
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
category = VALUES(category),
|
|
||||||
name = VALUES(name),
|
|
||||||
amount = VALUES(amount),
|
|
||||||
is_paid = VALUES(is_paid)
|
|
||||||
");
|
|
||||||
|
|
||||||
$keepIds = [];
|
$ch = curl_init();
|
||||||
for ($i = 0; $i < $count; $i++) {
|
curl_setopt($ch, CURLOPT_URL, $url);
|
||||||
$itemId = !empty($_POST['items']['id'][$i]) ? (int)$_POST['items']['id'][$i] : null;
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
$cat = $_POST['items']['cat'][$i] ?? 'activity';
|
curl_setopt($ch, CURLOPT_TIMEOUT, 3); // Timeout de 3s max pour ne pas bloquer l'UI
|
||||||
$name = trim($_POST['items']['name'][$i] ?? '');
|
$response = curl_exec($ch);
|
||||||
$amount = floatval($_POST['items']['amount'][$i] ?? 0);
|
curl_close($ch);
|
||||||
$paid = (int)($_POST['items']['paid'][$i] ?? 0);
|
|
||||||
$loc = !empty($_POST['items']['location'][$i]) ? $_POST['items']['location'][$i] : null;
|
|
||||||
|
|
||||||
if (!empty($name)) {
|
if ($response) {
|
||||||
$stmtItem->execute([$itemId, $id, $cat, $name, $amount, $paid, $loc]);
|
$data = json_decode($response, true);
|
||||||
$keepIds[] = $itemId ?: $pdo->lastInsertId();
|
if (!empty($data['hits'][0]['largeImageURL'])) {
|
||||||
|
$image_url = $data['hits'][0]['largeImageURL'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Nettoyage : On supprime les items qui ont été retirés de la modale
|
// 3. Sauvegarde en Base
|
||||||
// (Attention : uniquement ceux du voyage actuel qui ne sont plus dans la liste envoyée)
|
if ($id) {
|
||||||
if (!empty($keepIds)) {
|
// UPDATE
|
||||||
$placeholders = implode(',', array_fill(0, count($keepIds), '?'));
|
$sql = "UPDATE pf_holidays SET title=?, period_hint=?, start_date=?, end_date=?, status=?, budget_food=?, budget_extra=?, notes=?, vehicle_id=?, image_url=? WHERE id=?";
|
||||||
$sqlDel = "DELETE FROM pf_holidays_items WHERE holiday_id = ? AND id NOT IN ($placeholders)";
|
$stmt = $pdo->prepare($sql);
|
||||||
$pdo->prepare($sqlDel)->execute(array_merge([$id], $keepIds));
|
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes, $vehicle_id, $image_url, $id]);
|
||||||
}
|
} else {
|
||||||
|
// INSERT
|
||||||
|
$sql = "INSERT INTO pf_holidays (title, period_hint, start_date, end_date, status, budget_food, budget_extra, notes, vehicle_id, image_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes, $vehicle_id, $image_url]);
|
||||||
|
$id = $pdo->lastInsertId();
|
||||||
}
|
}
|
||||||
|
|
||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
@@ -84,6 +83,5 @@ try {
|
|||||||
die("Erreur base de données : " . $e->getMessage());
|
die("Erreur base de données : " . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirection vers la page principale
|
|
||||||
header("Location: /holidays.php");
|
header("Location: /holidays.php");
|
||||||
exit;
|
exit;
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
// modules/holidays/includes/api/upload_attachment.php
|
||||||
|
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||||
|
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||||
|
require_login();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Méthode non autorisée']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$holiday_id = isset($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : 0;
|
||||||
|
$item_id = (isset($_POST['item_id']) && (int)$_POST['item_id'] > 0) ? (int)$_POST['item_id'] : null;
|
||||||
|
|
||||||
|
if ($holiday_id === 0 || empty($_FILES['file'])) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Données manquantes ou fichier invalide']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $_FILES['file'];
|
||||||
|
|
||||||
|
// On range les fichiers dans un sous-dossier par voyage pour rester propre sur le NAS
|
||||||
|
$uploadDir = dirname(__DIR__, 4) . '/uploads/holidays/' . $holiday_id . '/';
|
||||||
|
|
||||||
|
if (!is_dir($uploadDir)) {
|
||||||
|
mkdir($uploadDir, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sécurisation du nom de fichier (retrait des accents et caractères spéciaux)
|
||||||
|
$fileName = basename($file['name']);
|
||||||
|
$safeFileName = preg_replace("/[^a-zA-Z0-9.-]/", "_", $fileName);
|
||||||
|
$uniqueFileName = time() . '_' . $safeFileName;
|
||||||
|
$destination = $uploadDir . $uniqueFileName;
|
||||||
|
|
||||||
|
if (move_uploaded_file($file['tmp_name'], $destination)) {
|
||||||
|
// Enregistrement en base de données
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pf_holidays_attachments (holiday_id, item_id, file_name, file_path) VALUES (?, ?, ?, ?)");
|
||||||
|
$stmt->execute([$holiday_id, $item_id, $fileName, 'uploads/holidays/' . $holiday_id . '/' . $uniqueFileName]);
|
||||||
|
|
||||||
|
$attachmentId = $pdo->lastInsertId();
|
||||||
|
echo json_encode(['success' => true, 'id' => $attachmentId, 'file_name' => $fileName]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Erreur lors de l\'écriture du fichier sur le NAS']);
|
||||||
|
}
|
||||||
+638
-252
@@ -11,11 +11,18 @@ if ($id === 0) {
|
|||||||
// Récupération des données du voyage
|
// Récupération des données du voyage
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
SELECT h.*,
|
SELECT h.*,
|
||||||
|
v.name as vehicle_name,
|
||||||
|
v.consumption as vehicle_consumption,
|
||||||
(COALESCE(h.budget_food, 0) + COALESCE(h.budget_extra, 0) + COALESCE((SELECT SUM(amount) FROM pf_holidays_items WHERE holiday_id = h.id), 0)) as total_cost,
|
(COALESCE(h.budget_food, 0) + COALESCE(h.budget_extra, 0) + COALESCE((SELECT SUM(amount) FROM pf_holidays_items WHERE holiday_id = h.id), 0)) as total_cost,
|
||||||
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND is_paid = 1) as total_paid,
|
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND is_paid = 1) as total_paid,
|
||||||
(SELECT COALESCE(SUM(amount), 0) FROM pf_savings WHERE holiday_id = h.id) as total_saved
|
(SELECT COALESCE(SUM(amount), 0) FROM pf_savings WHERE holiday_id = h.id) as total_saved,
|
||||||
FROM pf_holidays h WHERE h.id = ?
|
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND (expense_context = 'transit' OR (category = 'transport' AND (name LIKE '%Essence%' OR name LIKE '%Carburant%')))) as total_fuel,
|
||||||
|
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND category = 'transport' AND (name LIKE '%Péage%' OR name LIKE '%Peage%')) as total_tolls
|
||||||
|
FROM pf_holidays h
|
||||||
|
LEFT JOIN pf_vehicles v ON h.vehicle_id = v.id
|
||||||
|
WHERE h.id = ?
|
||||||
");
|
");
|
||||||
|
|
||||||
$stmt->execute([$id]);
|
$stmt->execute([$id]);
|
||||||
$holiday = $stmt->fetch(PDO::FETCH_ASSOC);
|
$holiday = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
@@ -42,7 +49,7 @@ foreach ($items as $it) {
|
|||||||
'sort_order' => $it['sort_order'],
|
'sort_order' => $it['sort_order'],
|
||||||
'step_start_date' => $it['step_start_date'],
|
'step_start_date' => $it['step_start_date'],
|
||||||
'step_end_date' => $it['step_end_date'],
|
'step_end_date' => $it['step_end_date'],
|
||||||
'is_return' => (int)$it['is_return'],
|
'step_type' => $it['step_type'] ?? 'stop',
|
||||||
'total_amount' => 0,
|
'total_amount' => 0,
|
||||||
'items' => []
|
'items' => []
|
||||||
];
|
];
|
||||||
@@ -59,8 +66,9 @@ $mapPoints = array_values($steps);
|
|||||||
// Affichage de la date
|
// Affichage de la date
|
||||||
$dateDisplay = htmlspecialchars($holiday['period_hint'] ?? '');
|
$dateDisplay = htmlspecialchars($holiday['period_hint'] ?? '');
|
||||||
if (empty($dateDisplay) && $holiday['start_date']) {
|
if (empty($dateDisplay) && $holiday['start_date']) {
|
||||||
$dateDisplay = date('d/m/Y', strtotime($holiday['start_date']));
|
// 💡 Format Jour/Mois uniquement
|
||||||
if ($holiday['end_date']) $dateDisplay .= ' → ' . date('d/m/Y', strtotime($holiday['end_date']));
|
$dateDisplay = date('d/m', strtotime($holiday['start_date']));
|
||||||
|
if ($holiday['end_date']) $dateDisplay .= ' → ' . date('d/m', strtotime($holiday['end_date']));
|
||||||
}
|
}
|
||||||
|
|
||||||
$cost = (float)$holiday['total_cost'];
|
$cost = (float)$holiday['total_cost'];
|
||||||
@@ -69,211 +77,259 @@ $saved = (float)$holiday['total_saved'];
|
|||||||
$leftToPay = max(0, $cost - $paid);
|
$leftToPay = max(0, $cost - $paid);
|
||||||
$pctPaid = $cost > 0 ? min(100, ($paid / $cost) * 100) : 0;
|
$pctPaid = $cost > 0 ? min(100, ($paid / $cost) * 100) : 0;
|
||||||
$pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
$pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||||
|
|
||||||
|
// Image de fond (API Pixabay ou fallback par défaut)
|
||||||
|
$bgImage = !empty($holiday['image_url']) ? htmlspecialchars($holiday['image_url']) : '/modules/home/assets/img/background.jpg';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.mobile-only { display: none !important; }
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.desktop-only { display: none !important; }
|
|
||||||
.mobile-only { display: flex !important; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="pf-holidays-detail">
|
<div class="pf-holidays-detail">
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 20px; flex-wrap: wrap; gap: 15px;">
|
<!-- Scripts JSON invisibles -->
|
||||||
<div style="display: flex; flex-direction: column; gap: 12px;">
|
<script id="holidayDataJson" type="application/json">
|
||||||
<a href="?tab=list" class="pf-btn btn-secondary pf-btn-small" style="width: fit-content; text-decoration: none;"><?= tr('btn_back') ?></a>
|
<?= json_encode(['main' => $holiday, 'items' => $generalItems], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>
|
||||||
<div style="display: flex; align-items: center; gap: 15px;">
|
</script>
|
||||||
<h1 style="margin: 0; font-size: 1.8rem;"><?= htmlspecialchars($holiday['title']) ?></h1>
|
|
||||||
<span class="hol-badge-status"><?= tr('hdl_status_' . $holiday['status']) ?></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<!-- 1. HERO HEADER IMMERSIF -->
|
||||||
<script id="holidayDataJson" type="application/json">
|
<div class="hol-hero-banner">
|
||||||
<?= json_encode(['main' => $holiday, 'items' => $generalItems], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>
|
<img src="<?= $bgImage ?>" class="hol-hero-bg-img" alt="">
|
||||||
</script>
|
<div class="hol-hero-overlay"></div>
|
||||||
<button type="button" class="pf-btn btn-secondary pf-btn-small" onclick="editHoliday(JSON.parse(document.getElementById('holidayDataJson').textContent))">
|
<div class="hol-hero-content">
|
||||||
<?= tr('btn_edit_bases') ?>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hol-summary-card">
|
<div class="hol-hero-top">
|
||||||
<div class="hol-summary-grid">
|
<a href="?tab=list" class="hol-hero-back-link">◀ <?= tr('btn_back') ?></a>
|
||||||
<div class="hol-summary-item">
|
<span class="hol-hero-status-badge"><?= tr('hdl_status_' . $holiday['status']) ?></span>
|
||||||
<div class="hol-summary-label"><?= tr('hdl_label_period') ?></div>
|
|
||||||
<div class="hol-summary-value"><?= $dateDisplay ?: tr('hdl_dates_to_define') ?></div>
|
|
||||||
</div>
|
|
||||||
<div class="hol-summary-item">
|
|
||||||
<div class="hol-summary-label"><?= tr('hdl_label_budget_food_extras') ?></div>
|
|
||||||
<div class="hol-summary-value">🍔 <?= number_format($holiday['budget_food'], 0) ?> € | 🎁 <?= number_format($holiday['budget_extra'], 0) ?> €</div>
|
|
||||||
</div>
|
|
||||||
<div class="hol-summary-item">
|
|
||||||
<div class="hol-summary-label"><?= tr('hdl_total_budget') ?></div>
|
|
||||||
<div class="hol-summary-value total"><?= number_format($cost, 0, ',', ' ') ?> €</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hol-progress-bar">
|
|
||||||
<div class="hol-progress-paid" style="width:<?= $pctPaid ?>%;" title="<?= tr('hdl_paid') ?>"></div>
|
|
||||||
<div class="hol-progress-saved" style="width:<?= $pctSaved ?>%;" title="<?= tr('hdl_saved') ?>"></div>
|
|
||||||
</div>
|
|
||||||
<div class="hol-progress-labels">
|
|
||||||
<span class="hol-label-paid">✓ <?= tr('hdl_paid') ?> : <?= number_format($paid, 0, ',', ' ') ?> €</span>
|
|
||||||
<span class="hol-label-saved">💼 <?= tr('hdl_saved') ?> : <?= number_format($saved, 0, ',', ' ') ?> €</span>
|
|
||||||
<span class="hol-label-left">⏳ <?= tr('hdl_left_to_pay') ?> : <?= number_format($leftToPay, 0, ',', ' ') ?> €</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hol-layout-grid">
|
|
||||||
<div class="hol-panel">
|
|
||||||
<div class="hol-panel-header" style="flex-wrap: wrap; gap: 10px;">
|
|
||||||
<div class="hol-panel-header-group">
|
|
||||||
<h3 style="margin:0;"><?= tr('hdl_map_itinerary') ?></h3>
|
|
||||||
<div class="hol-map-legend">
|
|
||||||
<span class="hol-legend-item" title="<?= tr('hdl_outbound') ?>"><span class="hol-legend-color aller"></span> <?= tr('hdl_outbound') ?></span>
|
|
||||||
<span class="hol-legend-item" title="<?= tr('hdl_return') ?>"><span class="hol-legend-color retour"></span> <?= tr('hdl_return') ?></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="pf-btn pf-btn-small" onclick="openCheckpointModal('add')"><?= tr('hdl_btn_add_step') ?></button>
|
|
||||||
</div>
|
|
||||||
<div id="tripMap" style="flex:1; width:100%; min-height: 400px; background:#f1f5f9; position: relative; z-index: 1;"></div> </div>
|
|
||||||
|
|
||||||
<div class="hol-panel">
|
|
||||||
<div class="hol-panel-header">
|
|
||||||
<h3 style="margin:0;"><?= tr('hdl_step_details') ?></h3>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hol-panel-body">
|
<h1 class="hol-hero-title"><?= htmlspecialchars($holiday['title']) ?></h1>
|
||||||
<?php if (empty($steps)): ?>
|
<div class="hol-hero-date-badge">
|
||||||
<p style="color:var(--text-muted); font-style:italic; text-align:center; margin-top:40px;"><?= tr('hdl_no_steps') ?></p>
|
🗓️ <?= $dateDisplay ?>
|
||||||
<?php else: ?>
|
</div>
|
||||||
|
|
||||||
<?php if (!empty($generalItems) || $holiday['budget_food'] > 0 || $holiday['budget_extra'] > 0): ?>
|
<!-- LES PILLS (Indicateurs Route & Quotidien) -->
|
||||||
<div class="hol-checkpoint" style="border-left-color: #64748b; background: #f8fafc; margin-bottom: 20px;">
|
<div class="hol-hero-pills">
|
||||||
<div class="hol-cp-header" style="background: transparent;">
|
|
||||||
<div class="hol-cp-info-group">
|
|
||||||
<span style="font-size:1.4rem;">🌍</span>
|
|
||||||
<div class="hol-cp-title"><?= tr('hdl_general_costs') ?></div>
|
|
||||||
</div>
|
|
||||||
<?php
|
|
||||||
$generalTotal = $holiday['budget_food'] + $holiday['budget_extra'];
|
|
||||||
foreach ($generalItems as $gi) { $generalTotal += $gi['amount']; }
|
|
||||||
?>
|
|
||||||
<div class="hol-cp-actions-group">
|
|
||||||
<div class="hol-cp-total"><?= number_format($generalTotal, 2, ',', ' ') ?> €</div>
|
|
||||||
<button onclick='editHoliday(JSON.parse(document.getElementById("holidayDataJson").textContent))' class="btn-icon-small" title="<?= tr('btn_edit') ?>">⚙️</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hol-cp-body">
|
<!-- PILL ROUTE (Masqué par défaut, géré par OSRM) -->
|
||||||
<?php if ($holiday['budget_food'] > 0): ?>
|
<span id="block_total_distance" class="hol-pill-block hol-hidden">
|
||||||
<div class="hol-expense-wrapper"><div class="hol-expense-main">
|
<span class="hol-pill">
|
||||||
<span class="hol-expense-name" style="color:#64748b;">🍔 <?= tr('hdl_food_bev') ?></span>
|
🛣️ <span id="global_total_distance">0</span> km (⏱️ <span id="global_total_duration">0h00</span>)
|
||||||
<span><strong class="hol-expense-amount"><?= number_format($holiday['budget_food'], 2, ',', ' ') ?> €</strong><span class="status-pending">⏳</span></span>
|
</span>
|
||||||
</div></div>
|
</span>
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($holiday['budget_extra'] > 0): ?>
|
|
||||||
<div class="hol-expense-wrapper"><div class="hol-expense-main">
|
|
||||||
<span class="hol-expense-name" style="color:#64748b;">🎁 <?= tr('hdl_extras') ?></span>
|
|
||||||
<span><strong class="hol-expense-amount"><?= number_format($holiday['budget_extra'], 2, ',', ' ') ?> €</strong><span class="status-pending">⏳</span></span>
|
|
||||||
</div></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php foreach ($generalItems as $it):
|
<!-- PILL PÉAGES -->
|
||||||
$icon = match($it['category']) { 'transport' => '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' };
|
<span class="hol-pill">
|
||||||
?>
|
💳 <?= number_format($holiday['total_tolls'], 0) ?> €
|
||||||
<div class="hol-expense-wrapper"><div class="hol-expense-main">
|
</span>
|
||||||
<span class="hol-expense-name"><?= $icon ?> <?= htmlspecialchars($it['name']) ?></span>
|
|
||||||
<span>
|
<!-- PILL ESSENCE (Avec Edit + Oeil) -->
|
||||||
<strong class="hol-expense-amount"><?= number_format($it['amount'], 2, ',', ' ') ?> €</strong>
|
<span class="hol-pill">
|
||||||
<span class="<?= $it['is_paid'] ? 'status-paid' : 'status-pending' ?>"><?= $it['is_paid'] ? '✓' : '⏳' ?></span>
|
⛽ <span id="global_fuel_cost"><?= number_format($holiday['total_fuel'], 0) ?></span> €
|
||||||
</span>
|
<span onclick="updateFuelPrice()" class="hol-pill-action" title="Modifier le prix de l'essence">(<span id="display_fuel_price">1.85</span>€/L) ✏️</span>
|
||||||
</div></div>
|
<?php if ($holiday['total_fuel'] > 0 || $holiday['total_tolls'] > 0): ?>
|
||||||
<?php endforeach; ?>
|
<span onclick="openTransitModal()" class="hol-pill-action" title="Voir le détail des trajets">👁️</span>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
|
||||||
<?php foreach ($steps as $step): ?>
|
<!-- PILL BURGER (Food) -->
|
||||||
<div id="step-card-<?= $step['sort_order'] ?>" class="hol-checkpoint hol-checkpoint-draggable" draggable="true" data-location="<?= htmlspecialchars($step['location_name']) ?>">
|
<span class="hol-pill">
|
||||||
<div class="hol-cp-header">
|
🍔 <?= number_format($holiday['budget_food'], 0) ?> €
|
||||||
<div class="hol-cp-info-group">
|
</span>
|
||||||
<span class="desktop-only" style="color:#94a3b8; font-size:1.1rem; cursor:grab; user-select:none;">☰</span>
|
|
||||||
<div class="mobile-only" style="flex-direction:column; gap:4px; margin-right:8px;">
|
<!-- PILL CADEAU (Extras) -->
|
||||||
<button type="button" onclick="moveStepMobile(this, -1)" class="btn-icon-small" style="width:24px!important; height:24px!important; font-size:0.6rem;">▲</button>
|
<span class="hol-pill">
|
||||||
<button type="button" onclick="moveStepMobile(this, 1)" class="btn-icon-small" style="width:24px!important; height:24px!important; font-size:0.6rem;">▼</button>
|
🎁 <?= number_format($holiday['budget_extra'], 0) ?> €
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FINANCES GLOBALES -->
|
||||||
|
<div class="hol-hero-finances">
|
||||||
|
<div class="hol-finance-block">
|
||||||
|
<div class="hol-finance-label"><?= tr('hdl_total_budget') ?></div>
|
||||||
|
<div class="hol-finance-value"><?= number_format($cost, 0, ',', ' ') ?> €</div>
|
||||||
|
</div>
|
||||||
|
<div class="hol-finance-block hol-finance-right">
|
||||||
|
<div class="hol-finance-label"><?= tr('hdl_left_to_pay') ?></div>
|
||||||
|
<div class="hol-finance-value hol-finance-value-due"><?= number_format($leftToPay, 0, ',', ' ') ?> €</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Barre de progression -->
|
||||||
|
<div class="hol-progress-bar">
|
||||||
|
<div class="hol-progress-paid" style="width: <?= $pctPaid ?>%;"></div>
|
||||||
|
<div class="hol-progress-saved" style="width: <?= $pctSaved ?>%;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="hol-hero-progress-labels">
|
||||||
|
<span class="hol-label-paid-text">✓ <?= tr('hdl_paid') ?> : <?= number_format($paid, 0, ',', ' ') ?> €</span>
|
||||||
|
<span class="hol-label-saved-text">💼 <?= tr('hdl_saved') ?> : <?= number_format($saved, 0, ',', ' ') ?> €</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. BARRE D'ACTIONS RAPIDES -->
|
||||||
|
<div class="hol-quick-actions-bar">
|
||||||
|
<button type="button" class="pf-btn btn-secondary pf-btn-small hol-quick-action-btn" onclick="toggleTripMap()">
|
||||||
|
🗺️ Carte
|
||||||
|
</button>
|
||||||
|
<button type="button" class="pf-btn btn-secondary pf-btn-small hol-quick-action-btn" onclick="openGlobalPlanningModal()">
|
||||||
|
📅 Planning
|
||||||
|
</button>
|
||||||
|
<button type="button" class="pf-btn btn-secondary pf-btn-small hol-quick-action-btn" onclick="generateTravelBook()">
|
||||||
|
📖 PDF
|
||||||
|
</button>
|
||||||
|
<button type="button" class="pf-btn btn-secondary pf-btn-small hol-quick-action-btn" onclick="editHoliday(JSON.parse(document.getElementById('holidayDataJson').textContent))">
|
||||||
|
⚙️ Modifier
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. LA CARTE (Masquée par défaut) -->
|
||||||
|
<div id="tripMapWrapper" class="hol-map-wrapper hol-hidden">
|
||||||
|
<div class="hol-panel-header hol-map-panel-header">
|
||||||
|
<h3 class="hol-map-title">Itinéraire</h3>
|
||||||
|
<div class="hol-map-legend">
|
||||||
|
<span class="hol-legend-item"><span class="hol-legend-color aller"></span> Aller</span>
|
||||||
|
<span class="hol-legend-item"><span class="hol-legend-color retour"></span> Retour</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- IMPORTANT : On garde l'ID tripMap pour que Leaflet (OSRM) fonctionne ! -->
|
||||||
|
<div id="tripMap" class="hol-map-canvas"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 4. TIMELINE (Carnet de route Pleine Largeur) -->
|
||||||
|
<div class="hol-panel hol-panel-timeline">
|
||||||
|
<div class="hol-panel-header hol-timeline-header">
|
||||||
|
<h3><?= tr('hdl_step_details') ?></h3>
|
||||||
|
<button class="pf-btn pf-btn-small" onclick="openCheckpointModal('add')">+ <?= tr('hdl_btn_add_step') ?></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hol-panel-body">
|
||||||
|
<?php if (empty($steps)): ?>
|
||||||
|
<p class="hol-empty-steps-text"><?= tr('hdl_no_steps') ?></p>
|
||||||
|
<?php else: ?>
|
||||||
|
|
||||||
|
<?php foreach ($steps as $step): ?>
|
||||||
|
<div id="step-card-<?= $step['sort_order'] ?>" class="hol-checkpoint hol-checkpoint-draggable" draggable="true" data-location="<?= htmlspecialchars($step['location_name']) ?>">
|
||||||
|
<div class="hol-step-header">
|
||||||
|
|
||||||
|
<div class="hol-step-row-top">
|
||||||
|
<?php
|
||||||
|
$stepIcon = '📍';
|
||||||
|
if ($step['step_type'] === 'origin') {
|
||||||
|
$stepIcon = '🛫';
|
||||||
|
} elseif ($step['step_type'] === 'destination') {
|
||||||
|
$stepIcon = '🛬';
|
||||||
|
}
|
||||||
|
$isReturn = ($holiday['return_step_id'] !== null && $holiday['return_step_id'] == $step['sort_order']);
|
||||||
|
?>
|
||||||
|
<div class="hol-step-icon hol-step-icon-<?= htmlspecialchars($step['step_type']) ?>">
|
||||||
|
<?= $stepIcon ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hol-step-title" onclick="panMapTo(<?= $step['lat'] ?>, <?= $step['lng'] ?>)" title="<?= htmlspecialchars($step['location_name']) ?>">
|
||||||
|
<?= htmlspecialchars($step['location_name']) ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hol-step-controls">
|
||||||
|
<span class="desktop-only hol-drag-handle">⣿</span>
|
||||||
|
<div class="mobile-only hol-step-arrows">
|
||||||
|
<button type="button" onclick="moveStepMobile(this, -1)">▲</button>
|
||||||
|
<button type="button" onclick="moveStepMobile(this, 1)">▼</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hol-step-row-bottom">
|
||||||
|
<div class="hol-step-meta">
|
||||||
|
<?php if ($isReturn): ?>
|
||||||
|
<span class="hol-tag-return">🏁 Retour</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($step['step_start_date']) && !empty($step['step_end_date'])): ?>
|
||||||
|
<span class="hol-step-date">
|
||||||
|
<?= date('d/m', strtotime($step['step_start_date'])) ?> ➔ <?= date('d/m', strtotime($step['step_end_date'])) ?>
|
||||||
|
</span>
|
||||||
|
<div class="hol-weather-info"></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hol-step-actions">
|
||||||
|
<span class="hol-step-price"><?= number_format($step['total_amount'], 2, ',', ' ') ?> €</span>
|
||||||
|
|
||||||
|
<button onclick="openGpsModal(<?= $step['lat'] ?>, <?= $step['lng'] ?>)"
|
||||||
|
class="btn-icon-small hol-btn-action-icon"
|
||||||
|
title="Y aller avec le GPS">
|
||||||
|
🧭
|
||||||
|
</button>
|
||||||
|
<button onclick="openDocsModal(<?= $step['sort_order'] ?>)" class="btn-icon-small hol-btn-action-icon" title="Documents & Billets">📎</button>
|
||||||
|
|
||||||
|
<button onclick='openCheckpointModal("edit", <?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small hol-btn-action-icon" title="<?= tr('btn_edit') ?>">✏️</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hol-cp-body">
|
||||||
|
<?php
|
||||||
|
$visibleItemsCount = 0;
|
||||||
|
foreach ($step['items'] as $it):
|
||||||
|
if ($it['name'] === 'PF_TECHNICAL_POINT') continue;
|
||||||
|
$visibleItemsCount++;
|
||||||
|
$icon = match($it['category']) { 'transport' => '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' };
|
||||||
|
|
||||||
|
// On vérifie si l'activité possède ses propres coordonnées GPS
|
||||||
|
$hasCustomGps = (!empty($it['lat']) && !empty($it['lng']) && ((float)$it['lat'] !== (float)$step['lat'] || (float)$it['lng'] !== (float)$step['lng']));
|
||||||
|
?>
|
||||||
|
<div class="hol-expense-wrapper">
|
||||||
|
<div class="hol-expense-main">
|
||||||
|
<span class="hol-expense-name">
|
||||||
|
<?= $icon ?>
|
||||||
|
<?php if ($hasCustomGps): ?>
|
||||||
|
<span onclick="openGpsModal(<?= $it['lat'] ?>, <?= $it['lng'] ?>)" class="hol-custom-gps-link" title="Y aller (GPS)">
|
||||||
|
<?= htmlspecialchars($it['name']) ?>
|
||||||
|
</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<?= htmlspecialchars($it['name']) ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<strong class="hol-expense-amount"><?= number_format($it['amount'], 2, ',', ' ') ?> €</strong>
|
||||||
|
<span class="<?= $it['is_paid'] ? 'status-paid' : 'status-pending' ?>" title="<?= $it['is_paid'] ? tr('hdl_paid') : tr('hdl_to_pay') ?>"><?= $it['is_paid'] ? '✓' : '⏳' ?></span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hol-cp-title" onclick="panMapTo(<?= $step['lat'] ?>, <?= $step['lng'] ?>)" title="<?= htmlspecialchars($step['location_name']) ?>">
|
<?php if (!empty($it['notes'])): ?>
|
||||||
📍 <?= htmlspecialchars($step['location_name']) ?>
|
<div class="hol-expense-note">
|
||||||
<?php if (!empty($step['is_return'])): ?>
|
<?= htmlspecialchars($it['notes']) ?>
|
||||||
<span style="background: #fff7ed; color: #ea580c; padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: bold; margin-left: 5px; border: 1px solid #ffedd5; vertical-align: middle;">🏁 <?= tr('hdl_return') ?></span>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (!empty($step['step_start_date']) && !empty($step['step_end_date'])): ?>
|
|
||||||
<div class="hol-date-weather-wrapper">
|
|
||||||
<span class="hol-step-dates">
|
|
||||||
<?= tr('hdl_from') ?> <?= date('d/m', strtotime($step['step_start_date'])) ?> <?= tr('hdl_to') ?> <?= date('d/m', strtotime($step['step_end_date'])) ?>
|
|
||||||
</span>
|
|
||||||
<div class="hol-weather-info"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<?php endforeach; ?>
|
||||||
<div class="hol-cp-actions-group">
|
|
||||||
<div class="hol-cp-total"><?= number_format($step['total_amount'], 2, ',', ' ') ?> €</div>
|
|
||||||
<button onclick='openPlanningModal(<?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="<?= tr('hdl_view_planning') ?>">📅</button>
|
|
||||||
<button onclick='openCheckpointModal("edit", <?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="<?= tr('btn_edit') ?>">✏️</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hol-cp-body">
|
<?php if ($visibleItemsCount === 0): ?>
|
||||||
<?php
|
<div class="hol-empty-step">📍 <?= tr('hdl_passing_point') ?></div>
|
||||||
$visibleItemsCount = 0;
|
<?php endif; ?>
|
||||||
foreach ($step['items'] as $it):
|
|
||||||
if ($it['name'] === 'PF_TECHNICAL_POINT') continue;
|
|
||||||
$visibleItemsCount++;
|
|
||||||
$icon = match($it['category']) { 'transport' => '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' };
|
|
||||||
?>
|
|
||||||
<div class="hol-expense-wrapper">
|
|
||||||
<div class="hol-expense-main">
|
|
||||||
<span class="hol-expense-name"><?= $icon ?> <?= htmlspecialchars($it['name']) ?></span>
|
|
||||||
<span>
|
|
||||||
<strong class="hol-expense-amount"><?= number_format($it['amount'], 2, ',', ' ') ?> €</strong>
|
|
||||||
<span class="<?= $it['is_paid'] ? 'status-paid' : 'status-pending' ?>" title="<?= $it['is_paid'] ? tr('hdl_paid') : tr('hdl_to_pay') ?>"><?= $it['is_paid'] ? '✓' : '⏳' ?></span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
|
|
||||||
<?php if ($visibleItemsCount === 0): ?>
|
|
||||||
<div class="hol-empty-step">📍 <?= tr('hdl_passing_point') ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pf-card hol-notes-card">
|
||||||
|
<h2 class="pf-card-title">📝 <?= tr('hdl_label_notes') ?></h2>
|
||||||
|
<div class="pf-card-body">
|
||||||
|
<textarea id="holidayGlobalNotes" class="pf-input hol-notes-textarea" rows="6" placeholder="<?= tr('hdl_ph_notes') ?>"><?= htmlspecialchars($holiday['notes'] ?? '') ?></textarea>
|
||||||
|
<div class="hol-notes-actions">
|
||||||
|
<button id="btnSaveHolidayNote" class="pf-btn" onclick="saveHolidayGlobalNote(<?= (int)$_GET['id'] ?>)">
|
||||||
|
💾 <?= tr('btn_save') ?>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if (!empty($holiday['notes'])): ?>
|
|
||||||
<div class="hol-summary-card" style="margin-top: 24px; padding: 25px; border-left: 5px solid #f59e0b;">
|
|
||||||
<h3 style="margin: 0 0 15px 0; font-size: 1.2rem; color: #0f172a; display: flex; align-items: center; gap: 8px;">
|
|
||||||
📝 <?= tr('hdl_label_notes') ?>
|
|
||||||
</h3>
|
|
||||||
<div style="font-size: 0.95rem; color: #334155; white-space: pre-wrap; line-height: 1.6;">
|
|
||||||
<?= htmlspecialchars($holiday['notes']) ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id="checkpointModal" class="pf-modal">
|
<div id="checkpointModal" class="pf-modal">
|
||||||
<div class="pf-modal-content" style="max-width: 600px;">
|
<div class="pf-modal-content" style="max-width: 600px;">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||||||
@@ -281,6 +337,12 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
<button type="button" onclick="closeCheckpointModal()" class="pf-modal-close">×</button>
|
<button type="button" onclick="closeCheckpointModal()" class="pf-modal-close">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- LES ONGLETS SONT ICI -->
|
||||||
|
<div style="display: flex; gap: 15px; border-bottom: 1px solid var(--border-light); margin-bottom: 20px;">
|
||||||
|
<button type="button" id="tabBtnInfo" onclick="switchCpTab('info')" style="background: none; border: none; font-weight: bold; font-size: 1rem; color: var(--primary); border-bottom: 2px solid var(--primary); padding-bottom: 8px; cursor: pointer; flex: 1;">📍 Infos de l'étape</button>
|
||||||
|
<button type="button" id="tabBtnProg" onclick="switchCpTab('prog')" style="background: none; border: none; font-weight: bold; font-size: 1rem; color: var(--text-muted); border-bottom: 2px solid transparent; padding-bottom: 8px; cursor: pointer; flex: 1;">🎟️ Programme & Frais</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="cpSearchBlock" style="margin-bottom:20px;">
|
<div id="cpSearchBlock" style="margin-bottom:20px;">
|
||||||
<?php if (!empty($favorites)): ?>
|
<?php if (!empty($favorites)): ?>
|
||||||
<div style="margin-bottom:15px; display:flex; gap:8px; flex-wrap:wrap;">
|
<div style="margin-bottom:15px; display:flex; gap:8px; flex-wrap:wrap;">
|
||||||
@@ -300,6 +362,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
<div id="searchResults" style="margin-top:10px; max-height:200px; overflow-y:auto;"></div>
|
<div id="searchResults" style="margin-top:10px; max-height:200px; overflow-y:auto;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- FORMULAIRE (Le contenu apparaîtra ici) -->
|
||||||
<form action="/modules/holidays/includes/api/save_checkpoint.php" method="POST" id="formCheckpoint" style="display:none; border-top:1px solid #e2e8f0; padding-top:20px;">
|
<form action="/modules/holidays/includes/api/save_checkpoint.php" method="POST" id="formCheckpoint" style="display:none; border-top:1px solid #e2e8f0; padding-top:20px;">
|
||||||
<input type="hidden" name="holiday_id" value="<?= $id ?>">
|
<input type="hidden" name="holiday_id" value="<?= $id ?>">
|
||||||
<input type="hidden" name="old_location_name" id="cp_old_name">
|
<input type="hidden" name="old_location_name" id="cp_old_name">
|
||||||
@@ -307,36 +370,52 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
<input type="hidden" name="lng" id="cp_lng">
|
<input type="hidden" name="lng" id="cp_lng">
|
||||||
<input type="hidden" name="old_sort_order" id="cp_old_sort_order">
|
<input type="hidden" name="old_sort_order" id="cp_old_sort_order">
|
||||||
|
|
||||||
<div class="form-group" style="margin-bottom:15px;">
|
<!-- CONTENU DE L'ONGLET INFO -->
|
||||||
<label class="pf-label"><?= tr('hdl_label_step_name') ?></label>
|
<div id="cpTabInfo">
|
||||||
<input type="text" name="location_name" id="cp_name" class="pf-input" style="font-weight:bold; color:var(--primary);" required>
|
<div class="form-group" style="margin-bottom:15px;">
|
||||||
</div>
|
<label class="pf-label"><?= tr('hdl_label_step_name') ?></label>
|
||||||
|
<input type="text" name="location_name" id="cp_name" class="pf-input" style="font-weight:bold; color:var(--primary);" required>
|
||||||
<div style="display:flex; gap:15px; margin-bottom:15px; background:#f8fafc; padding:12px; border-radius:8px;">
|
|
||||||
<div class="form-group" style="flex:1;">
|
|
||||||
<label class="pf-label"><?= tr('hdl_label_arrival') ?></label>
|
|
||||||
<input type="date" name="step_start_date" id="cp_start_date" class="pf-input">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="flex:1;">
|
|
||||||
<label class="pf-label"><?= tr('hdl_label_departure') ?></label>
|
<div class="form-group" style="margin-bottom:15px; background:#fff7ed; padding:10px; border-radius:8px; border:1px solid #ffedd5;">
|
||||||
<input type="date" name="step_end_date" id="cp_end_date" class="pf-input">
|
<label class="pf-label" style="color:#ea580c;">📍 Type d'étape</label>
|
||||||
|
<select name="step_type" id="cp_step_type" class="pf-input" onchange="toggleStepDates(this.value)">
|
||||||
|
<option value="origin">DÉPART (Point de départ du voyage)</option>
|
||||||
|
<option value="stop">SÉJOUR (Étape classique avec arrivée et départ)</option>
|
||||||
|
<option value="destination">ARRIVÉE FINALE (Fin du voyage)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex; gap:15px; margin-bottom:15px; background:#f8fafc; padding:12px; border-radius:8px;">
|
||||||
|
<div class="form-group" id="grp_start_date" style="flex:1;">
|
||||||
|
<label class="pf-label" id="lbl_start_date"><?= tr('hdl_label_arrival') ?></label>
|
||||||
|
<input type="date" name="step_start_date" id="cp_start_date" class="pf-input">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" id="grp_end_date" style="flex:1;">
|
||||||
|
<label class="pf-label" id="lbl_end_date"><?= tr('hdl_label_departure') ?></label>
|
||||||
|
<input type="date" name="step_end_date" id="cp_end_date" class="pf-input">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 20px; padding-top: 15px; border-top: 1px dashed #e2e8f0;">
|
||||||
|
<label style="display:flex; align-items:center; cursor:pointer; color:#ea580c; font-weight:600; font-size:0.9rem;">
|
||||||
|
<input type="checkbox" name="set_as_return" id="cp_set_as_return" value="1" style="margin-right:8px; width:16px; height:16px;">
|
||||||
|
🏁 Définir comme retour
|
||||||
|
</label>
|
||||||
|
<p style="margin: 4px 0 0 24px; font-size: 0.75rem; color: #64748b;">La route sera tracée en orange à partir d'ici.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-bottom: 15px;">
|
<!-- CONTENU DE L'ONGLET PROGRAMME & FRAIS -->
|
||||||
<label style="display:flex; align-items:center; cursor:pointer; color:#ea580c; font-weight:600;">
|
<div id="cpTabProg" class="hol-hidden" style="display: none;">
|
||||||
<input type="checkbox" name="is_return" id="cp_is_return" value="1" style="margin-right:8px;">
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
|
||||||
🏁 <?= tr('hdl_return') ?>
|
<label class="pf-label" style="margin:0;"><?= tr('hdl_planned_expenses') ?></label>
|
||||||
</label>
|
<button type="button" class="pf-btn btn-secondary pf-btn-small" onclick="addCpExpenseLine()"><?= tr('hdl_btn_add_expense') ?></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
|
<div id="cpExpensesContainer" style="margin-bottom:15px;"></div>
|
||||||
<label class="pf-label" style="margin:0;"><?= tr('hdl_planned_expenses') ?></label>
|
|
||||||
<button type="button" class="pf-btn btn-secondary pf-btn-small" onclick="addCpExpenseLine()"><?= tr('hdl_btn_add_expense') ?></button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="cpExpensesContainer" style="margin-bottom:15px;"></div>
|
|
||||||
|
|
||||||
<div style="margin-bottom: 20px;">
|
<div style="margin-bottom: 20px;">
|
||||||
<label style="display:flex; align-items:center; cursor:pointer; font-size:0.85rem; color:#475569;">
|
<label style="display:flex; align-items:center; cursor:pointer; font-size:0.85rem; color:#475569;">
|
||||||
<input type="checkbox" name="save_favorite" value="1" style="margin-right:8px;">
|
<input type="checkbox" name="save_favorite" value="1" style="margin-right:8px;">
|
||||||
@@ -354,67 +433,374 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="planningModal" class="pf-modal">
|
<div id="planningModal" class="pf-modal">
|
||||||
<div class="pf-modal-content" style="max-width: 550px;">
|
<div class="pf-modal-content hol-modal-planning">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
<div class="hol-modal-header">
|
||||||
<h3 id="planningModalTitle" style="margin:0; color:var(--primary);">📅 <?= tr('hdl_planning_title') ?></h3>
|
<h3 id="planningModalTitle" class="hol-modal-title">📅 Planning Global</h3>
|
||||||
<button type="button" onclick="closePlanningModal()" class="pf-modal-close">×</button>
|
<button type="button" onclick="closePlanningModal()" class="pf-modal-close">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="planningContainer" style="width: 100%;"></div>
|
|
||||||
|
<!-- Conteneur Injecté en JS -->
|
||||||
|
<div id="planningContainer" class="hol-planning-container"></div>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" onclick="closePlanningModal()" class="pf-btn btn-secondary"><?= tr('btn_close') ?></button>
|
<button type="button" onclick="closePlanningModal()" class="pf-btn btn-secondary"><?= tr('btn_close') ?></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="transitModal" class="pf-modal">
|
||||||
|
<div class="pf-modal-content hol-modal-sm">
|
||||||
|
<div class="hol-modal-header">
|
||||||
|
<h3 class="hol-modal-title">🛣️ Détail des trajets</h3>
|
||||||
|
<button type="button" onclick="closeTransitModal()" class="pf-modal-close">×</button>
|
||||||
|
</div>
|
||||||
|
<div id="transitDetailsContainer" class="hol-transit-container">
|
||||||
|
<p class="hol-empty-text">Calcul en cours...</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" onclick="closeTransitModal()" class="pf-btn btn-secondary"><?= tr('btn_close') ?></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="gpsModal" class="pf-modal">
|
||||||
|
<div class="pf-modal-content hol-modal-gps">
|
||||||
|
<div class="hol-modal-header">
|
||||||
|
<h3 class="hol-modal-title">🧭 Y aller avec...</h3>
|
||||||
|
<button type="button" onclick="closeGpsModal()" class="pf-modal-close">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hol-gps-buttons">
|
||||||
|
<button onclick="launchGpsApp('waze')" class="pf-btn btn-secondary hol-gps-btn">
|
||||||
|
<img src="https://cdn.simpleicons.org/waze/05C8F0" alt="Waze" class="hol-gps-icon">
|
||||||
|
Waze
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onclick="launchGpsApp('gmaps')" class="pf-btn btn-secondary hol-gps-btn">
|
||||||
|
<img src="https://cdn.simpleicons.org/googlemaps" alt="Google Maps" class="hol-gps-icon">
|
||||||
|
Google Maps
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onclick="launchGpsApp('amaps')" class="pf-btn btn-secondary hol-gps-btn">
|
||||||
|
<img src="https://cdn.simpleicons.org/apple/000000" alt="Apple Maps" class="hol-gps-icon">
|
||||||
|
Apple Maps
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="docsModal" class="pf-modal">
|
||||||
|
<div class="pf-modal-content hol-modal-docs">
|
||||||
|
<div class="hol-modal-header">
|
||||||
|
<h3 class="hol-modal-title">📎 Porte-documents</h3>
|
||||||
|
<button type="button" onclick="closeDocsModal()" class="pf-modal-close">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hol-docs-dropzone">
|
||||||
|
<p class="hol-docs-hint">Ajoutez vos billets, réservations ou PDFs pour cette étape.</p>
|
||||||
|
|
||||||
|
<input type="file" id="docFileInput" class="hol-hidden" accept=".pdf,.png,.jpg,.jpeg" onchange="handleFileUpload(this)">
|
||||||
|
|
||||||
|
<button type="button" class="pf-btn btn-primary hol-docs-select-btn" onclick="document.getElementById('docFileInput').click()">
|
||||||
|
+ Sélectionner un fichier
|
||||||
|
</button>
|
||||||
|
<div id="uploadStatus" class="hol-upload-status"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="docsListContainer" class="hol-docs-list">
|
||||||
|
<p class="hol-empty-text">Aucun document pour cette étape.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php include __DIR__ . '/modal.php'; ?>
|
<?php include __DIR__ . '/modal.php'; ?>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
|
||||||
|
<?php include __DIR__ . '/pdf_template.php'; ?>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// --- 1. SÉCURISATION TRADUCTIONS ET VARIABLES ---
|
// --- 1. SÉCURISATION TRADUCTIONS ET VARIABLES ---
|
||||||
window.MAP_POINTS = <?= json_encode($mapPoints ?? []) ?>;
|
window.MAP_POINTS = <?= json_encode($mapPoints ?? []) ?>;
|
||||||
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
||||||
|
|
||||||
window.I18N = {
|
window.I18N = {
|
||||||
...(window.I18N || {}),
|
...(window.I18N || {}),
|
||||||
'hdl_js_search_loading': "<?= tr('hdl_js_search_loading') ?>",
|
'hdl_js_search_loading': <?= json_encode(tr('hdl_js_search_loading')) ?>,
|
||||||
'hdl_js_no_result': "<?= tr('hdl_js_no_result') ?>",
|
'hdl_js_no_result': <?= json_encode(tr('hdl_js_no_result')) ?>,
|
||||||
'hdl_js_confirm_del_trip': "<?= tr('hdl_js_confirm_del_trip') ?>",
|
'hdl_js_confirm_del_trip': <?= json_encode(tr('hdl_js_confirm_del_trip')) ?>,
|
||||||
'hdl_js_confirm_del_step': "<?= tr('hdl_js_confirm_del_step') ?>",
|
'hdl_js_confirm_del_step': <?= json_encode(tr('hdl_js_confirm_del_step')) ?>,
|
||||||
'hdl_js_step_label': "<?= tr('hdl_js_step_label') ?>",
|
'hdl_js_step_label': <?= json_encode(tr('hdl_js_step_label')) ?>,
|
||||||
'hdl_js_ph_expense_name': "<?= tr('hdl_js_ph_expense_name') ?>",
|
'hdl_js_ph_expense_name': <?= json_encode(tr('hdl_js_ph_expense_name')) ?>,
|
||||||
'hdl_js_delete_line': "<?= tr('btn_delete') ?>",
|
'hdl_js_delete_line': <?= json_encode(tr('btn_delete')) ?>,
|
||||||
'hdl_planning_title': "<?= tr('hdl_planning_title') ?>",
|
'hdl_planning_title': <?= json_encode(tr('hdl_planning_title')) ?>,
|
||||||
'hdl_to_place': "<?= tr('hdl_to_place') ?>",
|
'hdl_to_place': <?= json_encode(tr('hdl_to_place')) ?>,
|
||||||
'hdl_js_missing_dates_title': "<?= tr('hdl_js_missing_dates_title') ?>",
|
'hdl_js_missing_dates_title': <?= json_encode(tr('hdl_js_missing_dates_title')) ?>,
|
||||||
'hdl_js_missing_dates_msg': "<?= tr('hdl_js_missing_dates_msg') ?>",
|
'hdl_js_missing_dates_msg': <?= json_encode(tr('hdl_js_missing_dates_msg')) ?>,
|
||||||
'hdl_modal_title': "<?= tr('hdl_modal_title') ?>",
|
'hdl_modal_title': <?= json_encode(tr('hdl_modal_title')) ?>,
|
||||||
'hdl_js_edit_step': "<?= tr('hdl_js_edit_step') ?>",
|
'hdl_js_edit_step': <?= json_encode(tr('hdl_js_edit_step')) ?>,
|
||||||
'hdl_ph_notes': "<?= tr('hdl_ph_notes') ?>",
|
'hdl_ph_notes': <?= json_encode(tr('hdl_ph_notes')) ?>,
|
||||||
'hdl_btn_add_step': "<?= tr('hdl_btn_add_step') ?>",
|
'hdl_btn_add_step': <?= json_encode(tr('hdl_btn_add_step')) ?>,
|
||||||
'hdl_quick_edit_title': "<?= tr('hdl_quick_edit_title') ?>",
|
'hdl_quick_edit_title': <?= json_encode(tr('hdl_quick_edit_title')) ?>,
|
||||||
'hdl_paid': "<?= tr('hdl_paid') ?>",
|
'hdl_paid': <?= json_encode(tr('hdl_paid')) ?>,
|
||||||
|
'weather_sunny': <?= json_encode(tr('weather_sunny')) ?>,
|
||||||
|
'weather_cloudy': <?= json_encode(tr('weather_cloudy')) ?>,
|
||||||
|
'weather_rainy': <?= json_encode(tr('weather_rainy')) ?>,
|
||||||
|
'weather_snowy': <?= json_encode(tr('weather_snowy')) ?>,
|
||||||
|
'weather_forecast': <?= json_encode(tr('weather_forecast')) ?>,
|
||||||
|
'weather_historical': <?= json_encode(tr('weather_historical')) ?>
|
||||||
|
};
|
||||||
|
|
||||||
|
const savedFuelPrice = localStorage.getItem('holidays_fuel_price') || 1.85;
|
||||||
|
window.FUEL_PRICE = parseFloat(savedFuelPrice);
|
||||||
|
|
||||||
// --- NOUVELLES CLÉS MÉTÉO ICI ---
|
const displayFuelEl = document.getElementById('display_fuel_price');
|
||||||
'weather_sunny': "<?= tr('weather_sunny') ?>",
|
if (displayFuelEl) displayFuelEl.innerText = window.FUEL_PRICE.toFixed(2);
|
||||||
'weather_cloudy': "<?= tr('weather_cloudy') ?>",
|
|
||||||
'weather_rainy': "<?= tr('weather_rainy') ?>",
|
|
||||||
'weather_snowy': "<?= tr('weather_snowy') ?>",
|
|
||||||
'weather_forecast': "<?= tr('weather_forecast') ?>",
|
|
||||||
'weather_historical': "<?= tr('weather_historical') ?>"
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fallback de sécurité pour s'assurer que les modales peuvent toujours se fermer
|
window.VEHICLE_CONSUMPTION = <?= !empty($holiday['vehicle_consumption']) ? (float)$holiday['vehicle_consumption'] : 7 ?>;
|
||||||
window.closeCheckpointModal = window.closeCheckpointModal || function() {
|
window.GLOBAL_RETURN_STEP_ID = <?= $holiday['return_step_id'] !== null ? $holiday['return_step_id'] : 'null' ?>;
|
||||||
const modal = document.getElementById('checkpointModal');
|
|
||||||
if(modal) modal.style.display = 'none';
|
|
||||||
document.body.classList.remove('no-scroll');
|
|
||||||
};
|
|
||||||
|
|
||||||
window.closePlanningModal = window.closePlanningModal || function() {
|
// --- 2. SURCHARGE SÉCURISÉE DES FONCTIONS JS (APRÈS CHARGEMENT DE HOLIDAYS.JS) ---
|
||||||
const modal = document.getElementById('planningModal');
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
if(modal) modal.style.display = 'none';
|
|
||||||
document.body.classList.remove('no-scroll');
|
// 1. Correction du conflit d'onglet
|
||||||
};
|
window.switchCpTab = function(tabId) {
|
||||||
|
const btnInfo = document.getElementById('tabBtnInfo');
|
||||||
|
const btnProg = document.getElementById('tabBtnProg');
|
||||||
|
const tabInfo = document.getElementById('cpTabInfo');
|
||||||
|
const tabProg = document.getElementById('cpTabProg');
|
||||||
|
|
||||||
|
if (tabId === 'info') {
|
||||||
|
if (btnInfo) { btnInfo.style.borderBottomColor = "var(--primary)"; btnInfo.style.color = "var(--primary)"; }
|
||||||
|
if (btnProg) { btnProg.style.borderBottomColor = "transparent"; btnProg.style.color = "var(--text-muted)"; }
|
||||||
|
if (tabInfo) { tabInfo.style.display = "block"; tabInfo.classList.remove("hol-hidden"); }
|
||||||
|
if (tabProg) { tabProg.style.display = "none"; tabProg.classList.add("hol-hidden"); }
|
||||||
|
} else {
|
||||||
|
if (btnProg) { btnProg.style.borderBottomColor = "var(--primary)"; btnProg.style.color = "var(--primary)"; }
|
||||||
|
if (btnInfo) { btnInfo.style.borderBottomColor = "transparent"; btnInfo.style.color = "var(--text-muted)"; }
|
||||||
|
if (tabProg) { tabProg.style.display = "block"; tabProg.classList.remove("hol-hidden"); }
|
||||||
|
if (tabInfo) { tabInfo.style.display = "none"; tabInfo.classList.add("hol-hidden"); }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Correction modale : ajout de l'injection lat/lng aux items
|
||||||
|
window.openCheckpointModal = function(mode, data = null) {
|
||||||
|
const searchBlock = document.getElementById("cpSearchBlock");
|
||||||
|
const formBlock = document.getElementById("formCheckpoint");
|
||||||
|
const container = document.getElementById("cpExpensesContainer");
|
||||||
|
const btnDel = document.getElementById("btnDeleteCp");
|
||||||
|
const modalTitle = document.getElementById("cpModalTitle");
|
||||||
|
|
||||||
|
if(container) container.innerHTML = "";
|
||||||
|
if (document.getElementById("cp_start_date")) document.getElementById("cp_start_date").value = "";
|
||||||
|
if (document.getElementById("cp_end_date")) document.getElementById("cp_end_date").value = "";
|
||||||
|
if (document.getElementById("searchPlaceInput")) document.getElementById("searchPlaceInput").value = "";
|
||||||
|
if (document.getElementById("searchResults")) document.getElementById("searchResults").innerHTML = "";
|
||||||
|
|
||||||
|
window.switchCpTab('info');
|
||||||
|
|
||||||
|
if (mode === "add") {
|
||||||
|
if (modalTitle) modalTitle.innerText = "📍 " + (window.I18N['hdl_btn_add_step'] || "Ajouter une étape");
|
||||||
|
if (searchBlock) searchBlock.style.display = "block";
|
||||||
|
if (formBlock) formBlock.style.display = "block";
|
||||||
|
if (btnDel) btnDel.style.display = "none";
|
||||||
|
|
||||||
|
if (document.getElementById("cp_old_sort_order")) document.getElementById("cp_old_sort_order").value = "";
|
||||||
|
if (document.getElementById("cp_name")) document.getElementById("cp_name").value = "";
|
||||||
|
|
||||||
|
if (document.getElementById("cp_step_type")) {
|
||||||
|
document.getElementById("cp_step_type").value = "stop";
|
||||||
|
if (typeof toggleStepDates === 'function') toggleStepDates("stop");
|
||||||
|
}
|
||||||
|
if (document.getElementById("cp_set_as_return")) document.getElementById("cp_set_as_return").checked = false;
|
||||||
|
|
||||||
|
if (typeof window.addCpExpenseLine === 'function') window.addCpExpenseLine();
|
||||||
|
|
||||||
|
} else if (mode === "edit" && data) {
|
||||||
|
if (modalTitle) modalTitle.innerText = "✏️ " + (window.I18N['hdl_js_edit_step'] || "Modifier l'étape");
|
||||||
|
if (searchBlock) searchBlock.style.display = "none";
|
||||||
|
if (formBlock) formBlock.style.display = "block";
|
||||||
|
if (btnDel) btnDel.style.display = "block";
|
||||||
|
|
||||||
|
if (document.getElementById("cp_lat")) document.getElementById("cp_lat").value = data.lat;
|
||||||
|
if (document.getElementById("cp_lng")) document.getElementById("cp_lng").value = data.lng;
|
||||||
|
if (document.getElementById("cp_old_sort_order")) document.getElementById("cp_old_sort_order").value = data.sort_order;
|
||||||
|
if (document.getElementById("cp_name")) document.getElementById("cp_name").value = data.location_name;
|
||||||
|
if (document.getElementById("cp_start_date")) document.getElementById("cp_start_date").value = data.step_start_date || "";
|
||||||
|
if (document.getElementById("cp_end_date")) document.getElementById("cp_end_date").value = data.step_end_date || "";
|
||||||
|
|
||||||
|
if (document.getElementById("cp_step_type")) {
|
||||||
|
const type = data.step_type || "stop";
|
||||||
|
document.getElementById("cp_step_type").value = type;
|
||||||
|
if (typeof toggleStepDates === 'function') toggleStepDates(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.getElementById("cp_set_as_return")) {
|
||||||
|
document.getElementById("cp_set_as_return").checked = (window.GLOBAL_RETURN_STEP_ID == data.sort_order);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Génération avec transmission de la latitude et longitude de chaque dépense
|
||||||
|
if (data.items && data.items.length > 0) {
|
||||||
|
let visibleCount = 0;
|
||||||
|
data.items.forEach((it) => {
|
||||||
|
if (it.name !== "PF_TECHNICAL_POINT") {
|
||||||
|
if (typeof window.addCpExpenseLine === 'function') {
|
||||||
|
window.addCpExpenseLine(
|
||||||
|
it.category, it.name, it.amount, it.is_paid, it.notes || "",
|
||||||
|
it.id || "", it.item_date || "", it.item_time || "", it.duration || 1,
|
||||||
|
it.expense_context || "local", it.lat || "", it.lng || ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
visibleCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (visibleCount === 0 && typeof window.addCpExpenseLine === 'function') window.addCpExpenseLine();
|
||||||
|
} else {
|
||||||
|
if (typeof window.addCpExpenseLine === 'function') window.addCpExpenseLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof cpDateRangePicker !== 'undefined' && cpDateRangePicker && typeof cpDateRangePicker.destroy === 'function') {
|
||||||
|
cpDateRangePicker.destroy();
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
const modal = document.getElementById("checkpointModal");
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = "flex";
|
||||||
|
document.body.classList.add("no-scroll");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. LA FONCTION AVEC LA CLASSE CSS PARFAITE ET LE BOUTON 📍
|
||||||
|
window.addCpExpenseLine = function(category = "accommodation", name = "", amount = "", isPaid = 0, notes = "", itemId = "", itemDate = "", itemTime = "", itemDur = 1, expenseContext = "local", itemLat = "", itemLng = "") {
|
||||||
|
const container = document.getElementById("cpExpensesContainer");
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.className = "hol-form-row"; // Force l'utilisation du CSS
|
||||||
|
const isChecked = isPaid == 1 ? "checked" : "";
|
||||||
|
|
||||||
|
// Indicateur visuel (bouton vert) si on a déjà des coordonnées pour cette activité
|
||||||
|
const pinBtnStyle = (itemLat && itemLng) ? "background:var(--success);color:white;border-color:var(--success);" : "";
|
||||||
|
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="hol-form-inner">
|
||||||
|
<select name="items[cat][]" class="pf-input hol-form-select">
|
||||||
|
<option value="accommodation" ${category === "accommodation" ? "selected" : ""}>🏨</option>
|
||||||
|
<option value="transport" ${category === "transport" ? "selected" : ""}>🚗</option>
|
||||||
|
<option value="activity" ${category === "activity" ? "selected" : ""}>🎫</option>
|
||||||
|
</select>
|
||||||
|
<input type="hidden" name="items[context][]" value="${expenseContext}">
|
||||||
|
|
||||||
|
<input type="text" name="items[name][]" class="pf-input hol-form-text" placeholder="${window.I18N['hdl_js_ph_expense_name'] || 'Nom de la dépense'}" value="${name}" required>
|
||||||
|
|
||||||
|
<button type="button" class="btn-icon-action" style="width:28px;height:28px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border-light);border-radius:6px; margin:0; flex-shrink:0; ${pinBtnStyle}" onclick="window.toggleItemSearch(this)" title="Adresse précise">📍</button>
|
||||||
|
|
||||||
|
<input type="number" step="0.01" name="items[amount][]" class="pf-input hol-form-number" placeholder="0.00" value="${amount}" required>
|
||||||
|
|
||||||
|
<label class="hol-form-paid-label" title="${window.I18N['hdl_paid'] || 'Payé'}">
|
||||||
|
<input type="checkbox" ${isChecked} onchange="this.nextElementSibling.value = this.checked ? 1 : 0" style="width:16px;height:16px;margin:0;accent-color:var(--primary);">
|
||||||
|
<input type="hidden" name="items[paid][]" value="${isPaid}">
|
||||||
|
<span class="hol-form-paid-text">Payé</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button type="button" onclick="this.parentElement.parentElement.remove()" title="${window.I18N['hdl_js_delete_line'] || 'Supprimer'}" class="btn-icon-action delete btn-remove-expense" style="margin: 0;">🗑️</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hol-item-search-box" style="display:none; padding: 10px; background: var(--bg-subtle); border-radius: 8px; margin-top: 6px; width: 100%;">
|
||||||
|
<div style="display:flex; gap:10px;">
|
||||||
|
<input type="text" class="pf-input item-search-input" placeholder="Chercher une adresse..." onkeypress="if(event.key === 'Enter') { window.searchItemPlace(this); return false; }">
|
||||||
|
<button type="button" class="pf-btn btn-secondary" onclick="window.searchItemPlace(this)">🔍</button>
|
||||||
|
</div>
|
||||||
|
<div class="item-search-results" style="margin-top:10px; max-height:150px; overflow-y:auto;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hol-form-subrow" style="display: flex; padding-left: 58px; margin-top: 6px;">
|
||||||
|
<input type="text" name="items[notes][]" class="pf-input hol-form-notes-input" placeholder="${window.I18N['hdl_ph_notes'] || 'Notes...'}" value="${notes}" style="font-size: 0.8rem; padding: 6px 8px; border-style: dashed; color: var(--text-muted); width: 100%; background: transparent; margin: 0;">
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="items[id][]" value="${itemId}">
|
||||||
|
<input type="hidden" name="items[date][]" value="${itemDate}">
|
||||||
|
<input type="hidden" name="items[time][]" value="${itemTime}">
|
||||||
|
<input type="hidden" name="items[duration][]" value="${itemDur}">
|
||||||
|
<input type="hidden" name="items[lat][]" value="${itemLat}">
|
||||||
|
<input type="hidden" name="items[lng][]" value="${itemLng}">
|
||||||
|
`;
|
||||||
|
container.appendChild(div);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. RÉINTÉGRATION DES FONCTIONS DE GÉOCODAGE DANS LE MÊME SCOPE
|
||||||
|
window.toggleItemSearch = function(btn) {
|
||||||
|
const searchBox = btn.closest(".hol-form-row").querySelector(".hol-item-search-box");
|
||||||
|
searchBox.style.display = searchBox.style.display === "none" ? "block" : "none";
|
||||||
|
};
|
||||||
|
|
||||||
|
window.searchItemPlace = async function(btn) {
|
||||||
|
const container = btn.closest(".hol-item-search-box");
|
||||||
|
const input = container.querySelector(".item-search-input");
|
||||||
|
const resultsDiv = container.querySelector(".item-search-results");
|
||||||
|
const q = input.value.trim();
|
||||||
|
if (q.length < 3) return;
|
||||||
|
|
||||||
|
resultsDiv.innerHTML = `<span style="color:#64748b; font-size:0.85rem;">Recherche en cours...</span>`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await pachaFetch("/modules/holidays/includes/api/geocode.php?limit=5&q=" + encodeURIComponent(q), { method: 'GET' });
|
||||||
|
resultsDiv.innerHTML = "";
|
||||||
|
if (data.error || !data.results || data.results.length === 0) {
|
||||||
|
resultsDiv.innerHTML = `<span style="color:#ef4444; font-size:0.85rem;">Aucun résultat</span>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.results.forEach((place) => {
|
||||||
|
const b = document.createElement("button");
|
||||||
|
b.type = "button";
|
||||||
|
b.className = "pf-btn btn-secondary";
|
||||||
|
b.style.textAlign = "left";
|
||||||
|
b.style.padding = "8px";
|
||||||
|
b.style.marginBottom = "4px";
|
||||||
|
b.style.width = "100%";
|
||||||
|
b.innerText = "📍 " + place.display_name;
|
||||||
|
b.onclick = () => window.selectItemPlace(container, place.lat, place.lng, place.display_name);
|
||||||
|
resultsDiv.appendChild(b);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
resultsDiv.innerHTML = `<span style="color:#ef4444; font-size:0.85rem;">Erreur de connexion</span>`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.selectItemPlace = function(container, lat, lng, displayName) {
|
||||||
|
const row = container.closest(".hol-form-row");
|
||||||
|
row.querySelector('input[name="items[lat][]"]').value = lat;
|
||||||
|
row.querySelector('input[name="items[lng][]"]').value = lng;
|
||||||
|
|
||||||
|
// Si le champ note est vide, on l'utilise pour y placer le nom complet de l'adresse
|
||||||
|
const notesInput = row.querySelector('input[name="items[notes][]"]');
|
||||||
|
if (notesInput.value === "") {
|
||||||
|
notesInput.value = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.style.display = "none";
|
||||||
|
|
||||||
|
// Le bouton devient vert pour confirmer la présence de coordonnées
|
||||||
|
const pinBtn = row.querySelector('.btn-icon-action[title="Adresse précise"]');
|
||||||
|
if (pinBtn) {
|
||||||
|
pinBtn.style.background = "var(--success)";
|
||||||
|
pinBtn.style.color = "white";
|
||||||
|
pinBtn.style.borderColor = "var(--success)";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.closeCheckpointModal = function() {
|
||||||
|
const modal = document.getElementById('checkpointModal');
|
||||||
|
if(modal) modal.style.display = 'none';
|
||||||
|
document.body.classList.remove('no-scroll');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.closePlanningModal = function() {
|
||||||
|
const modal = document.getElementById('planningModal');
|
||||||
|
if(modal) modal.style.display = 'none';
|
||||||
|
document.body.classList.remove('no-scroll');
|
||||||
|
};
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="/modules/holidays/holidays.js"></script>
|
|
||||||
@@ -122,9 +122,10 @@ window.closeHolidayModal = window.closeHolidayModal || function() {
|
|||||||
if(modal) modal.style.display = 'none';
|
if(modal) modal.style.display = 'none';
|
||||||
document.body.classList.remove('no-scroll');
|
document.body.classList.remove('no-scroll');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="/modules/holidays/holidays.js"></script>
|
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
function renderHolidayCard($h, $pdo) {
|
function renderHolidayCard($h, $pdo) {
|
||||||
@@ -136,8 +137,9 @@ function renderHolidayCard($h, $pdo) {
|
|||||||
$dateDisplay = htmlspecialchars($h['period_hint'] ?? '');
|
$dateDisplay = htmlspecialchars($h['period_hint'] ?? '');
|
||||||
|
|
||||||
if (empty($dateDisplay) && $h['start_date']) {
|
if (empty($dateDisplay) && $h['start_date']) {
|
||||||
$dateDisplay = date('d/m/Y', strtotime($h['start_date']));
|
// 💡 Format Jour/Mois uniquement
|
||||||
if ($h['end_date']) $dateDisplay .= ' → ' . date('d/m/Y', strtotime($h['end_date']));
|
$dateDisplay = date('d/m', strtotime($h['start_date']));
|
||||||
|
if ($h['end_date']) $dateDisplay .= ' → ' . date('d/m', strtotime($h['end_date']));
|
||||||
}
|
}
|
||||||
|
|
||||||
$statusClass = match($h['status']) {
|
$statusClass = match($h['status']) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div id="holidayModal" class="pf-modal">
|
<div id="holidayModal" class="pf-modal">
|
||||||
<div class="pf-modal-content hol-modal-content">
|
<div class="pf-modal-content hol-modal-content" style="max-width: 500px;">
|
||||||
<h3 id="modalTitle" class="pf-modal-title"><?= tr('hdl_modal_title') ?></h3>
|
<h3 id="modalTitle" class="pf-modal-title"><?= tr('hdl_modal_title') ?></h3>
|
||||||
|
|
||||||
<form action="/modules/holidays/includes/api/save_holiday.php" method="POST" id="holidayForm">
|
<form action="/modules/holidays/includes/api/save_holiday.php" method="POST" id="holidayForm">
|
||||||
@@ -24,68 +24,43 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div>
|
<div class="hol-flex-1">
|
||||||
<label class="pf-label"><?= tr('hdl_label_period') ?></label>
|
<label class="pf-label"><?= tr('hdl_label_period') ?></label>
|
||||||
<input type="text" name="period_hint" id="inp_period" class="pf-input" placeholder="<?= tr('hdl_ph_period') ?>">
|
<input type="text" name="period_hint" id="inp_period" class="pf-input" placeholder="<?= tr('hdl_ph_period') ?>">
|
||||||
</div>
|
</div>
|
||||||
<div class="hol-date-range-group">
|
<!-- DATES AVEC FLATPICKR -->
|
||||||
<div class="hol-flex-1">
|
<div class="hol-flex-2">
|
||||||
<label class="pf-label"><?= tr('hdl_label_from') ?></label>
|
<label class="pf-label">📅 Dates du voyage</label>
|
||||||
<input type="date" name="start_date" id="inp_start" class="pf-input">
|
<input type="text" id="hol_date_range" class="pf-input" placeholder="Sélectionnez les dates..." readonly style="cursor: pointer; background-color: var(--bg-panel); color: var(--primary); font-weight: bold; text-align: center; letter-spacing: 0.5px;">
|
||||||
</div>
|
<!-- Vraies données pour save_holiday.php -->
|
||||||
<div class="hol-flex-1">
|
<input type="hidden" name="start_date" id="inp_start">
|
||||||
<label class="pf-label"><?= tr('hdl_label_to') ?></label>
|
<input type="hidden" name="end_date" id="inp_end">
|
||||||
<input type="date" name="end_date" id="inp_end" class="pf-input">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="hol-divider">
|
<div class="form-group" style="margin-top: 15px; padding: 10px; background: var(--bg-subtle); border-radius: 8px; border: 1px solid var(--border-light);">
|
||||||
|
<label class="pf-label" style="margin-bottom: 5px;">🚗 Véhicule utilisé (Optionnel)</label>
|
||||||
<div class="hol-columns-wrapper">
|
<select name="vehicle_id" id="inp_vehicle_id" class="pf-input">
|
||||||
<div class="hol-col">
|
<option value="">-- Aucun / Autre transport --</option>
|
||||||
<div class="hol-col-header">
|
<?php foreach($garageVehicles as $v): ?>
|
||||||
<h4 class="hol-cat-transport">🚗 <?= tr('hdl_cat_transport') ?></h4>
|
<option value="<?= $v['id'] ?>"><?= htmlspecialchars($v['name']) ?></option>
|
||||||
<button type="button" class="btn-add-item" onclick="addItem('transport')" title="<?= tr('hdl_add_transport') ?>">+</button>
|
<?php endforeach; ?>
|
||||||
</div>
|
</select>
|
||||||
<div id="list_transport" class="dynamic-list"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hol-col">
|
|
||||||
<div class="hol-col-header">
|
|
||||||
<h4 class="hol-cat-accommodation">🏨 <?= tr('hdl_cat_accommodation') ?></h4>
|
|
||||||
<button type="button" class="btn-add-item" onclick="addItem('accommodation')" title="<?= tr('hdl_add_accommodation') ?>">+</button>
|
|
||||||
</div>
|
|
||||||
<div id="list_accommodation" class="dynamic-list"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hol-col">
|
|
||||||
<div class="hol-col-header">
|
|
||||||
<h4 class="hol-cat-activity">🎫 <?= tr('hdl_cat_activity') ?></h4>
|
|
||||||
<button type="button" class="btn-add-item" onclick="addItem('activity')" title="<?= tr('hdl_add_activity') ?>">+</button>
|
|
||||||
</div>
|
|
||||||
<div id="list_activity" class="dynamic-list"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="hol-divider">
|
<hr class="hol-divider">
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div>
|
<div class="hol-flex-1">
|
||||||
<label class="pf-label">🍔 <?= tr('hdl_label_budget_food') ?></label>
|
<label class="pf-label">🍔 <?= tr('hdl_label_budget_food') ?></label>
|
||||||
<input type="number" step="0.01" name="budget_food" id="inp_food" class="pf-input" placeholder="0.00">
|
<input type="number" step="0.01" name="budget_food" id="inp_food" class="pf-input" placeholder="0.00">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="hol-flex-1">
|
||||||
<label class="pf-label">🎁 <?= tr('hdl_label_budget_extras') ?></label>
|
<label class="pf-label">🎁 <?= tr('hdl_label_budget_extras') ?></label>
|
||||||
<input type="number" step="0.01" name="budget_extra" id="inp_extra" class="pf-input" placeholder="0.00">
|
<input type="number" step="0.01" name="budget_extra" id="inp_extra" class="pf-input" placeholder="0.00">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="pf-label"><?= tr('hdl_label_notes') ?></label>
|
|
||||||
<textarea name="notes" id="inp_notes" class="pf-input" rows="2" placeholder="<?= tr('hdl_ph_notes') ?>"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" onclick="deleteHoliday()" id="btn_delete" class="pf-btn btn-secondary hol-btn-delete"><?= tr('btn_delete') ?></button>
|
<button type="button" onclick="deleteHoliday()" id="btn_delete" class="pf-btn btn-secondary hol-btn-delete"><?= tr('btn_delete') ?></button>
|
||||||
<button type="button" onclick="closeHolidayModal()" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
|
<button type="button" onclick="closeHolidayModal()" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
<?php
|
||||||
|
// Calcul du budget global pour la page de synthèse
|
||||||
|
$totalGeneral = $holiday['budget_food'] + $holiday['budget_extra'];
|
||||||
|
foreach ($generalItems as $gi) {
|
||||||
|
$totalGeneral += $gi['amount'];
|
||||||
|
}
|
||||||
|
$totalSteps = 0;
|
||||||
|
foreach ($steps as $step) {
|
||||||
|
$totalSteps += $step['total_amount'];
|
||||||
|
}
|
||||||
|
$grandTotal = $totalGeneral + $totalSteps;
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div style="display: none;">
|
||||||
|
<div id="travelBookTemplate" style="padding: 40px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #1e293b; background: white; width: 100%; max-width: 800px; margin: 0 auto;">
|
||||||
|
|
||||||
|
<div style="text-align: center; padding-top: 50px; margin-bottom: 60px;">
|
||||||
|
<div style="font-size: 4rem; margin-bottom: 20px;">🗺️</div>
|
||||||
|
<h1 style="font-size: 2.5rem; color: #0f172a; margin-bottom: 10px;"><?= htmlspecialchars($holiday['title'] ?? $holiday['name'] ?? 'Mon Voyage') ?></h1>
|
||||||
|
<h2 style="font-size: 1.5rem; color: #64748b; font-weight: normal; margin-top: 0;">
|
||||||
|
<?php if (!empty($holiday['start_date']) && !empty($holiday['end_date'])): ?>
|
||||||
|
Du <?= date('d/m', strtotime($holiday['start_date'])) ?> au <?= date('d/m', strtotime($holiday['end_date'])) ?>
|
||||||
|
<?php else: ?>
|
||||||
|
Dates à définir
|
||||||
|
<?php endif; ?>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 20px; margin-bottom: 40px; page-break-inside: avoid;">
|
||||||
|
<h3 style="margin-top: 0; color: #0f172a; border-bottom: 2px solid #cbd5e1; padding-bottom: 10px;">💰 Budget Prévisionnel : <?= number_format($grandTotal, 2, ',', ' ') ?> €</h3>
|
||||||
|
<table style="width: 100%; border-collapse: collapse; margin-top: 15px;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; border-bottom: 1px solid #e2e8f0; color: #475569;">Frais Généraux (Vols, Locations globales...)</td>
|
||||||
|
<td style="padding: 8px 0; border-bottom: 1px solid #e2e8f0; text-align: right; font-weight: bold;"><?= number_format($totalGeneral, 2, ',', ' ') ?> €</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; color: #475569;">Étapes (Hébergements, Activités...)</td>
|
||||||
|
<td style="padding: 8px 0; text-align: right; font-weight: bold;"><?= number_format($totalSteps, 2, ',', ' ') ?> €</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($generalItems) || $holiday['budget_food'] > 0 || $holiday['budget_extra'] > 0): ?>
|
||||||
|
<div style="margin-bottom: 40px; page-break-inside: avoid;">
|
||||||
|
<h3 style="color: #0f172a; border-bottom: 2px solid #0ea5e9; padding-bottom: 5px;">🌍 Réservations & Frais Généraux</h3>
|
||||||
|
<ul style="list-style-type: none; padding-left: 0; margin-top: 15px;">
|
||||||
|
<?php if ($holiday['budget_food'] > 0): ?>
|
||||||
|
<li style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">🍔 Budget Nourriture : <strong><?= number_format($holiday['budget_food'], 2, ',', ' ') ?> €</strong></li>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($holiday['budget_extra'] > 0): ?>
|
||||||
|
<li style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">🎁 Extras & Souvenirs : <strong><?= number_format($holiday['budget_extra'], 2, ',', ' ') ?> €</strong></li>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php foreach ($generalItems as $gi):
|
||||||
|
$icon = match($gi['category']) { 'transport' => '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' };
|
||||||
|
?>
|
||||||
|
<li style="padding: 8px 0; border-bottom: 1px solid #f1f5f9; display: flex; justify-content: space-between;">
|
||||||
|
<span><?= $icon ?> <?= htmlspecialchars($gi['name']) ?></span>
|
||||||
|
<span><strong><?= number_format($gi['amount'], 2, ',', ' ') ?> €</strong> <span style="font-size: 0.85em; color: #64748b;"><?= $gi['is_paid'] ? '(Payé ✓)' : '(À payer ⏳)' ?></span></span>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div style="page-break-before: always;"></div>
|
||||||
|
<h2 style="color: #0f172a; font-size: 2rem; border-bottom: 3px solid #0f172a; padding-bottom: 10px; margin-bottom: 30px;">📍 Itinéraire Détaillé</h2>
|
||||||
|
|
||||||
|
<?php foreach ($steps as $index => $step): ?>
|
||||||
|
|
||||||
|
<?php if ($index > 0): ?>
|
||||||
|
<div style="text-align: center; padding: 10px 0; color: #94a3b8; page-break-inside: avoid;">
|
||||||
|
<div style="border-left: 2px dashed #cbd5e1; height: 25px; margin: 0 auto; width: 2px;"></div>
|
||||||
|
<div style="margin: 5px 0; font-size: 0.9rem;">🚗 <em>En route vers l'étape suivante...</em></div>
|
||||||
|
<div style="border-left: 2px dashed #cbd5e1; height: 25px; margin: 0 auto; width: 2px;"></div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div style="background: #ffffff; border: 1px solid #e2e8f0; border-left: 6px solid #0ea5e9; border-radius: 8px; padding: 20px; margin-bottom: 0px; page-break-inside: avoid;">
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 15px;">
|
||||||
|
<div>
|
||||||
|
<h3 style="margin: 0; font-size: 1.4rem; color: #0f172a;"><?= htmlspecialchars($step['location_name']) ?></h3>
|
||||||
|
<?php if (!empty($step['step_start_date']) && !empty($step['step_end_date'])): ?>
|
||||||
|
<div style="color: #0ea5e9; margin-top: 5px; font-weight: bold; font-size: 0.95rem;">
|
||||||
|
📅 Du <?= date('d/m', strtotime($step['step_start_date'])) ?> au <?= date('d/m', strtotime($step['step_end_date'])) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: right;">
|
||||||
|
<span style="background: #f1f5f9; padding: 5px 10px; border-radius: 6px; font-weight: bold; color: #0f172a;">
|
||||||
|
<?= number_format($step['total_amount'], 2, ',', ' ') ?> €
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php $checkpoints = isset($step['items']) ? $step['items'] : []; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($checkpoints)): ?>
|
||||||
|
<div style="margin-top: 15px; border-top: 1px solid #f8fafc; padding-top: 15px;">
|
||||||
|
<h4 style="margin-top: 0; margin-bottom: 10px; color: #475569; font-size: 1rem;">📋 Planning & Activités :</h4>
|
||||||
|
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem;">
|
||||||
|
<?php foreach ($checkpoints as $cp):
|
||||||
|
$cpIcon = match($cp['category']) { 'transport' => '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' };
|
||||||
|
// Sécurité sur la date d'activité (item_date ou date)
|
||||||
|
$cpDate = !empty($cp['item_date']) ? $cp['item_date'] : (!empty($cp['date']) ? $cp['date'] : null);
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 6px 0; border-bottom: 1px solid #f8fafc; color: #64748b; width: 80px;">
|
||||||
|
<?= $cpDate ? date('d/m', strtotime($cpDate)) : '---' ?>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 6px 0; border-bottom: 1px solid #f8fafc; color: #334155;">
|
||||||
|
<?= $cpIcon ?> <?= htmlspecialchars($cp['name']) ?>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 6px 0; border-bottom: 1px solid #f8fafc; text-align: right; font-weight: bold; color: #0f172a;">
|
||||||
|
<?= number_format($cp['amount'], 2, ',', ' ') ?> €
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<div style="page-break-before: always;"></div>
|
||||||
|
<h2 style="color: #0f172a; font-size: 2rem; border-bottom: 3px solid #0f172a; padding-bottom: 10px; margin-bottom: 30px;">📝 Notes & Informations utiles</h2>
|
||||||
|
|
||||||
|
<p style="color: #64748b; margin-bottom: 40px; font-style: italic;">Espace réservé pour vos numéros d'urgence, codes de cadenas, adresses locales...</p>
|
||||||
|
|
||||||
|
<?php for ($i = 0; $i < 15; $i++): ?>
|
||||||
|
<div style="border-bottom: 1px dotted #cbd5e1; height: 35px; width: 100%;"></div>
|
||||||
|
<?php endfor; ?>
|
||||||
|
|
||||||
|
<div style="text-align: center; margin-top: 60px; font-size: 0.85rem; color: #94a3b8;">
|
||||||
|
Carnet de route généré automatiquement par <strong>HouseHub</strong>. Bon voyage ! ✈️
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,439 +0,0 @@
|
|||||||
/* ── Module Liste ─────────────────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.liste-main {
|
|
||||||
max-width: 680px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 1rem 1rem 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Tabs bar ──────────────────────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.liste-tabs-bar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: .4rem;
|
|
||||||
margin-bottom: .75rem;
|
|
||||||
overflow-x: auto;
|
|
||||||
padding-bottom: 2px;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
.liste-tabs-bar::-webkit-scrollbar { display: none; }
|
|
||||||
|
|
||||||
.liste-tabs {
|
|
||||||
display: flex;
|
|
||||||
gap: .35rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.liste-tab {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: .25rem;
|
|
||||||
padding: .4rem .75rem;
|
|
||||||
border-radius: 20px;
|
|
||||||
border: 1px solid var(--border, #dee2e6);
|
|
||||||
background: var(--card-bg, #fff);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: .875rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
transition: background .15s, border-color .15s;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.liste-tab:hover { background: var(--hover-bg, #f8f9fa); }
|
|
||||||
.liste-tab.active {
|
|
||||||
background: var(--primary, #4361ee);
|
|
||||||
color: #fff;
|
|
||||||
border-color: var(--primary, #4361ee);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.liste-tab-name { pointer-events: none; }
|
|
||||||
|
|
||||||
.liste-tab-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
padding: 0 .1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: .8rem;
|
|
||||||
color: rgba(255,255,255,.75);
|
|
||||||
line-height: 1;
|
|
||||||
transition: color .15s;
|
|
||||||
}
|
|
||||||
.liste-tab-btn:hover { color: #fff; }
|
|
||||||
|
|
||||||
.liste-tab-add {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 2px dashed var(--border, #dee2e6);
|
|
||||||
background: none;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: var(--muted, #6c757d);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: border-color .15s, color .15s;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
.liste-tab-add:hover {
|
|
||||||
border-color: var(--primary, #4361ee);
|
|
||||||
color: var(--primary, #4361ee);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── New list form ─────────────────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.liste-new-form {
|
|
||||||
display: flex;
|
|
||||||
gap: .5rem;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: .75rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.liste-new-form input {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 160px;
|
|
||||||
padding: .45rem .75rem;
|
|
||||||
border: 1px solid var(--primary, #4361ee);
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: .9rem;
|
|
||||||
background: var(--input-bg, #fff);
|
|
||||||
color: var(--text, #212529);
|
|
||||||
}
|
|
||||||
.liste-new-form button {
|
|
||||||
padding: .45rem .9rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: .875rem;
|
|
||||||
}
|
|
||||||
.liste-new-form #btn-confirm-new-list {
|
|
||||||
background: var(--primary, #4361ee);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.liste-new-form #btn-cancel-new-list {
|
|
||||||
background: var(--muted-bg, #e9ecef);
|
|
||||||
color: var(--text, #212529);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden { display: none !important; }
|
|
||||||
|
|
||||||
/* ── Add card ──────────────────────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.liste-add-card {
|
|
||||||
display: flex;
|
|
||||||
gap: .5rem;
|
|
||||||
background: var(--card-bg, #fff);
|
|
||||||
border: 1px solid var(--border, #dee2e6);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: .6rem .75rem;
|
|
||||||
margin-bottom: .75rem;
|
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,.05);
|
|
||||||
}
|
|
||||||
.liste-add-card input {
|
|
||||||
flex: 1;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
font-size: .95rem;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text, #212529);
|
|
||||||
}
|
|
||||||
.liste-add-card button {
|
|
||||||
background: var(--primary, #4361ee);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: .4rem .9rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: .875rem;
|
|
||||||
font-weight: 600;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Toolbar ───────────────────────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.liste-toolbar {
|
|
||||||
display: flex;
|
|
||||||
gap: .4rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: .75rem;
|
|
||||||
}
|
|
||||||
.btn-tool {
|
|
||||||
padding: .35rem .75rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--border, #dee2e6);
|
|
||||||
background: var(--card-bg, #fff);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: .8rem;
|
|
||||||
color: var(--text, #212529);
|
|
||||||
transition: background .15s;
|
|
||||||
}
|
|
||||||
.btn-tool:hover { background: var(--hover-bg, #f8f9fa); }
|
|
||||||
.btn-tool-danger { color: var(--danger, #e63946); border-color: var(--danger, #e63946); }
|
|
||||||
.btn-tool-danger:hover { background: rgba(230,57,70,.08); }
|
|
||||||
|
|
||||||
/* ── Items ─────────────────────────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.liste-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: .5rem;
|
|
||||||
padding: .55rem .75rem;
|
|
||||||
background: var(--card-bg, #fff);
|
|
||||||
border: 1px solid var(--border, #dee2e6);
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-bottom: .4rem;
|
|
||||||
transition: opacity .2s;
|
|
||||||
}
|
|
||||||
.liste-row.in-cart { opacity: .55; }
|
|
||||||
.liste-row.in-cart .liste-label { text-decoration: line-through; }
|
|
||||||
|
|
||||||
.liste-check-label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: .6rem;
|
|
||||||
flex: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.liste-check {
|
|
||||||
width: 1.1rem;
|
|
||||||
height: 1.1rem;
|
|
||||||
accent-color: var(--primary, #4361ee);
|
|
||||||
cursor: pointer;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.liste-label { font-size: .95rem; }
|
|
||||||
|
|
||||||
.liste-row-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: .25rem;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity .15s;
|
|
||||||
}
|
|
||||||
.liste-row:hover .liste-row-actions,
|
|
||||||
.liste-row:focus-within .liste-row-actions { opacity: 1; }
|
|
||||||
|
|
||||||
.btn-edit-item, .btn-delete-item {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: .2rem .3rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: .9rem;
|
|
||||||
transition: background .15s;
|
|
||||||
}
|
|
||||||
.btn-edit-item:hover { background: var(--hover-bg, #f0f0f0); }
|
|
||||||
.btn-delete-item:hover { background: rgba(230,57,70,.1); }
|
|
||||||
|
|
||||||
.liste-cart-divider {
|
|
||||||
font-size: .78rem;
|
|
||||||
color: var(--muted, #6c757d);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: .04em;
|
|
||||||
padding: .5rem .25rem .25rem;
|
|
||||||
margin-top: .25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.liste-empty-items {
|
|
||||||
text-align: center;
|
|
||||||
padding: 2rem 1rem;
|
|
||||||
color: var(--muted, #6c757d);
|
|
||||||
}
|
|
||||||
.liste-empty-items span { font-size: 2rem; display: block; margin-bottom: .5rem; }
|
|
||||||
|
|
||||||
/* ── History ───────────────────────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.liste-history-card {
|
|
||||||
margin-top: 1.25rem;
|
|
||||||
padding: .75rem;
|
|
||||||
background: var(--card-bg, #fff);
|
|
||||||
border: 1px solid var(--border, #dee2e6);
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
.liste-history-title {
|
|
||||||
font-size: .75rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: .05em;
|
|
||||||
color: var(--muted, #6c757d);
|
|
||||||
margin-bottom: .5rem;
|
|
||||||
}
|
|
||||||
.liste-history-chips { display: flex; flex-wrap: wrap; gap: .35rem; }
|
|
||||||
.liste-chip {
|
|
||||||
padding: .25rem .65rem;
|
|
||||||
border-radius: 20px;
|
|
||||||
border: 1px solid var(--border, #dee2e6);
|
|
||||||
background: var(--muted-bg, #f8f9fa);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: .82rem;
|
|
||||||
color: var(--text, #212529);
|
|
||||||
transition: background .15s, border-color .15s;
|
|
||||||
}
|
|
||||||
.liste-chip:hover {
|
|
||||||
background: var(--primary-light, #eef0fd);
|
|
||||||
border-color: var(--primary, #4361ee);
|
|
||||||
color: var(--primary, #4361ee);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Toast ─────────────────────────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.liste-toast-container {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 1.5rem;
|
|
||||||
right: 1rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: .4rem;
|
|
||||||
z-index: 9999;
|
|
||||||
}
|
|
||||||
.liste-toast {
|
|
||||||
padding: .55rem 1rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: .875rem;
|
|
||||||
background: #333;
|
|
||||||
color: #fff;
|
|
||||||
box-shadow: 0 3px 12px rgba(0,0,0,.2);
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(8px);
|
|
||||||
transition: opacity .2s, transform .2s;
|
|
||||||
}
|
|
||||||
.liste-toast.show { opacity: 1; transform: translateY(0); }
|
|
||||||
.liste-toast-warn { background: #f0a500; }
|
|
||||||
.liste-toast-error { background: #e63946; }
|
|
||||||
|
|
||||||
/* ── Empty state (no lists at all) ────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.liste-empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 3rem 1rem;
|
|
||||||
color: var(--muted, #6c757d);
|
|
||||||
}
|
|
||||||
.liste-empty-icon { font-size: 3rem; margin-bottom: .75rem; }
|
|
||||||
|
|
||||||
/* ── Category section headers ──────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.liste-cat-section-header {
|
|
||||||
font-size: .75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--muted, #6c757d);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: .05em;
|
|
||||||
padding: .6rem .25rem .2rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: .4rem;
|
|
||||||
}
|
|
||||||
.liste-cat-section-header:first-child { padding-top: .1rem; }
|
|
||||||
|
|
||||||
.liste-cat-section-count {
|
|
||||||
background: var(--muted-bg, #e9ecef);
|
|
||||||
color: var(--muted, #6c757d);
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: .7rem;
|
|
||||||
padding: .05rem .4rem;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0;
|
|
||||||
text-transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Category badge (per-item) ─────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.liste-cat-badge {
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: none;
|
|
||||||
border: 1px solid var(--border, #dee2e6);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: .1rem .3rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: .85rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
color: inherit;
|
|
||||||
transition: border-color .15s, background .15s;
|
|
||||||
}
|
|
||||||
.liste-cat-badge:hover {
|
|
||||||
border-color: var(--primary, #4361ee);
|
|
||||||
background: var(--primary-light, #eef0fd);
|
|
||||||
}
|
|
||||||
.liste-cat-badge-empty {
|
|
||||||
opacity: .35;
|
|
||||||
border-style: dashed;
|
|
||||||
}
|
|
||||||
.liste-row:hover .liste-cat-badge-empty { opacity: .7; }
|
|
||||||
|
|
||||||
/* ── Category picker popover ───────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.liste-cat-picker {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 9999;
|
|
||||||
background: var(--card-bg, #fff);
|
|
||||||
border: 1px solid var(--border, #dee2e6);
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 8px 32px rgba(0,0,0,.15);
|
|
||||||
width: 216px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.liste-cat-picker-title {
|
|
||||||
font-size: .72rem;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: .05em;
|
|
||||||
color: var(--muted, #6c757d);
|
|
||||||
padding: .6rem .75rem .35rem;
|
|
||||||
border-bottom: 1px solid var(--border, #dee2e6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.liste-cat-picker-list {
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 300px;
|
|
||||||
padding: .3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.liste-cat-picker-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: .4rem;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: .4rem .6rem;
|
|
||||||
font-size: .83rem;
|
|
||||||
color: var(--text, #212529);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background .1s;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
.liste-cat-picker-item:hover { background: var(--hover-bg, #f8f9fa); }
|
|
||||||
.liste-cat-picker-item.active {
|
|
||||||
background: var(--primary-light, #eef0fd);
|
|
||||||
color: var(--primary, #4361ee);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Dark mode ─────────────────────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
[data-theme='dark'] .liste-tab { background: #1e1e2e; border-color: #3a3a4e; color: #cdd6f4; }
|
|
||||||
[data-theme='dark'] .liste-tab:hover { background: #2a2a3e; }
|
|
||||||
[data-theme='dark'] .liste-tab.active { background: #4361ee; color: #fff; border-color: #4361ee; }
|
|
||||||
[data-theme='dark'] .liste-add-card,
|
|
||||||
[data-theme='dark'] .liste-row,
|
|
||||||
[data-theme='dark'] .liste-history-card { background: #1e1e2e; border-color: #3a3a4e; }
|
|
||||||
[data-theme='dark'] .liste-add-card input { color: #cdd6f4; }
|
|
||||||
[data-theme='dark'] .liste-chip { background: #2a2a3e; border-color: #3a3a4e; color: #cdd6f4; }
|
|
||||||
[data-theme='dark'] .liste-chip:hover { background: #313155; border-color: #4361ee; color: #89b4fa; }
|
|
||||||
[data-theme='dark'] .btn-tool { background: #1e1e2e; border-color: #3a3a4e; color: #cdd6f4; }
|
|
||||||
[data-theme='dark'] .btn-tool:hover { background: #2a2a3e; }
|
|
||||||
[data-theme='dark'] .liste-new-form input { background: #1e1e2e; color: #cdd6f4; border-color: #4361ee; }
|
|
||||||
[data-theme='dark'] .liste-new-form #btn-cancel-new-list { background: #2a2a3e; color: #cdd6f4; }
|
|
||||||
[data-theme='dark'] .liste-cat-badge { border-color: #3a3a4e; }
|
|
||||||
[data-theme='dark'] .liste-cat-badge:hover { background: #313155; border-color: #4361ee; }
|
|
||||||
[data-theme='dark'] .liste-cat-section-count { background: #2a2a3e; color: #8892b0; }
|
|
||||||
[data-theme='dark'] .liste-cat-picker { background: #1e1e2e; border-color: #3a3a4e; }
|
|
||||||
[data-theme='dark'] .liste-cat-picker-title { border-color: #3a3a4e; }
|
|
||||||
[data-theme='dark'] .liste-cat-picker-item { color: #cdd6f4; }
|
|
||||||
[data-theme='dark'] .liste-cat-picker-item:hover { background: #2a2a3e; }
|
|
||||||
[data-theme='dark'] .liste-cat-picker-item.active { background: #313155; color: #89b4fa; }
|
|
||||||
@@ -153,7 +153,7 @@ function renderAttachmentsView(atts,noteId){
|
|||||||
<div class="memo-att-title">🖼️ Images</div>
|
<div class="memo-att-title">🖼️ Images</div>
|
||||||
<div class="att-img-grid">${images.map(a=>`
|
<div class="att-img-grid">${images.map(a=>`
|
||||||
<div class="att-img-item" onclick="openLightbox('${API}?action=file&id=${a.id}')">
|
<div class="att-img-item" onclick="openLightbox('${API}?action=file&id=${a.id}')">
|
||||||
<img src="${API}?action=file&id=${a.id}" loading="lazy" alt="${escHtml(a.original_name||'')}">
|
<img src="${API}?action=file&id=${a.id}" alt="${escHtml(a.original_name||'')}">
|
||||||
<button class="att-del" onclick="event.stopPropagation();deleteAttachment(${a.id},${noteId})" title="Supprimer">✕</button>
|
<button class="att-del" onclick="event.stopPropagation();deleteAttachment(${a.id},${noteId})" title="Supprimer">✕</button>
|
||||||
</div>`).join('')}</div></div>`;
|
</div>`).join('')}</div></div>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
<?php
|
||||||
|
// modules/voyage/api.php
|
||||||
|
// Toll cost estimator API for HouseHub
|
||||||
|
|
||||||
|
require_once dirname(__DIR__, 2) . '/includes/auth.php';
|
||||||
|
require_login();
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$action = $_GET['action'] ?? '';
|
||||||
|
|
||||||
|
if ($action !== 'estimate') {
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Action inconnue']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Read POST body ---
|
||||||
|
$body = file_get_contents('php://input');
|
||||||
|
$params = json_decode($body, true);
|
||||||
|
if (!$params || empty($params['stops']) || count($params['stops']) < 2) {
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Paramètres invalides (stops requis, min 2)']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stops = $params['stops'];
|
||||||
|
$fuelL100 = (float)($params['fuel_l100'] ?? 7);
|
||||||
|
$fuelPrice= (float)($params['fuel_price'] ?? 1.85);
|
||||||
|
|
||||||
|
// --- Load pre-indexed PHP database ---
|
||||||
|
$tollsDataPath = __DIR__ . '/data/tolls.data.php';
|
||||||
|
$gpsPath = __DIR__ . '/data/toll_gps.json';
|
||||||
|
|
||||||
|
if (!file_exists($tollsDataPath)) {
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Base de péages introuvable']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grâce à OPcache, le fichier est exécuté et stocké en RAM partagée
|
||||||
|
$tollIndex = require $tollsDataPath;
|
||||||
|
|
||||||
|
$gpsData = [];
|
||||||
|
if (file_exists($gpsPath)) {
|
||||||
|
$gpsData = json_decode(file_get_contents($gpsPath), true) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helper: great-circle distance (km) ---
|
||||||
|
function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float {
|
||||||
|
$R = 6371.0;
|
||||||
|
$dLat = deg2rad($lat2 - $lat1);
|
||||||
|
$dLng = deg2rad($lng2 - $lng1);
|
||||||
|
$a = sin($dLat/2)**2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng/2)**2;
|
||||||
|
return $R * 2 * atan2(sqrt($a), sqrt(1 - $a));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Find plazas near a coordinate (within $maxKm) ---
|
||||||
|
// Returns array of [plaza_name => distance_km]
|
||||||
|
function plazasNear(array $gpsData, float $lat, float $lng, float $maxKm): array {
|
||||||
|
$result = [];
|
||||||
|
foreach ($gpsData as $name => $info) {
|
||||||
|
$d = haversine($lat, $lng, $info['lat'], $info['lng']);
|
||||||
|
if ($d <= $maxKm) {
|
||||||
|
$result[$name] = $d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
asort($result);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Look up toll price for (op, entry, exit) with fallback ---
|
||||||
|
function lookupToll(array $tollIndex, string $op, string $entry, string $exit): ?float {
|
||||||
|
// Direct lookup
|
||||||
|
if (isset($tollIndex[$op][$entry][$exit])) {
|
||||||
|
return $tollIndex[$op][$entry][$exit];
|
||||||
|
}
|
||||||
|
// Reversed
|
||||||
|
if (isset($tollIndex[$op][$exit][$entry])) {
|
||||||
|
return $tollIndex[$op][$exit][$entry];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- OSRM route distance ---
|
||||||
|
function getRouteDistanceKm(float $lat1, float $lng1, float $lat2, float $lng2): ?float {
|
||||||
|
$url = sprintf(
|
||||||
|
'https://router.project-osrm.org/route/v1/driving/%f,%f;%f,%f?overview=false',
|
||||||
|
$lng1, $lat1, $lng2, $lat2
|
||||||
|
);
|
||||||
|
$ctx = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'timeout' => 10,
|
||||||
|
'header' => "User-Agent: HouseHub/1.0\r\n"
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
$resp = @file_get_contents($url, false, $ctx);
|
||||||
|
if ($resp === false) return null;
|
||||||
|
$data = json_decode($resp, true);
|
||||||
|
if (!empty($data['routes'][0]['distance'])) {
|
||||||
|
return (float)$data['routes'][0]['distance'] / 1000.0;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Main: process each consecutive stop pair ---
|
||||||
|
$segments = [];
|
||||||
|
$totalKm = 0.0;
|
||||||
|
$totalToll = 0.0;
|
||||||
|
|
||||||
|
for ($i = 0; $i < count($stops) - 1; $i++) {
|
||||||
|
$orig = $stops[$i];
|
||||||
|
$dest = $stops[$i + 1];
|
||||||
|
|
||||||
|
$oLat = (float)$orig['lat'];
|
||||||
|
$oLng = (float)$orig['lng'];
|
||||||
|
$dLat = (float)$dest['lat'];
|
||||||
|
$dLng = (float)$dest['lng'];
|
||||||
|
$fromName = $orig['name'] ?? "Étape " . ($i + 1);
|
||||||
|
$toName = $dest['name'] ?? "Étape " . ($i + 2);
|
||||||
|
|
||||||
|
// Distance via OSRM
|
||||||
|
$distKm = getRouteDistanceKm($oLat, $oLng, $dLat, $dLng);
|
||||||
|
if ($distKm === null) {
|
||||||
|
// Fallback: straight-line distance * 1.25
|
||||||
|
$distKm = haversine($oLat, $oLng, $dLat, $dLng) * 1.25;
|
||||||
|
}
|
||||||
|
$totalKm += $distKm;
|
||||||
|
|
||||||
|
// --- Find toll ---
|
||||||
|
$segToll = 0.0;
|
||||||
|
$entryPlaza = null;
|
||||||
|
$exitPlaza = null;
|
||||||
|
$tollNote = null;
|
||||||
|
|
||||||
|
if (!empty($gpsData)) {
|
||||||
|
// Find plazas near origin (80km) and destination (80km)
|
||||||
|
$nearOrig = plazasNear($gpsData, $oLat, $oLng, 80.0);
|
||||||
|
$nearDest = plazasNear($gpsData, $dLat, $dLng, 80.0);
|
||||||
|
|
||||||
|
$bestToll = null;
|
||||||
|
$bestDistSum = PHP_FLOAT_MAX;
|
||||||
|
$bestEntry = null;
|
||||||
|
$bestExit = null;
|
||||||
|
$bestOp = null;
|
||||||
|
|
||||||
|
// Try each operator
|
||||||
|
foreach ($tollIndex as $op => $opEntries) {
|
||||||
|
// Find candidate entry plazas (near origin, present in this operator)
|
||||||
|
$candidateEntries = [];
|
||||||
|
foreach ($nearOrig as $pName => $dist) {
|
||||||
|
if (isset($opEntries[$pName]) || isset(array_flip(array_keys($opEntries))[$pName])) {
|
||||||
|
// Plaza exists as entry in this operator
|
||||||
|
if (isset($opEntries[$pName])) {
|
||||||
|
$candidateEntries[$pName] = $dist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Also check if it's an exit plaza (reversed lookup later)
|
||||||
|
// Include any plaza from this operator that is within range
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build set of all plazas for this operator
|
||||||
|
$opPlazas = [];
|
||||||
|
foreach ($opEntries as $entry => $exits) {
|
||||||
|
$opPlazas[$entry] = true;
|
||||||
|
foreach ($exits as $exit => $price) {
|
||||||
|
$opPlazas[$exit] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter nearOrig and nearDest to only plazas in this operator
|
||||||
|
$opNearOrig = array_intersect_key($nearOrig, $opPlazas);
|
||||||
|
$opNearDest = array_intersect_key($nearDest, $opPlazas);
|
||||||
|
|
||||||
|
if (empty($opNearOrig) || empty($opNearDest)) continue;
|
||||||
|
|
||||||
|
// Try best 5 entry and best 5 exit candidates
|
||||||
|
$topEntries = array_slice($opNearOrig, 0, 5, true);
|
||||||
|
$topExits = array_slice($opNearDest, 0, 5, true);
|
||||||
|
|
||||||
|
foreach ($topEntries as $ePlaza => $eDist) {
|
||||||
|
foreach ($topExits as $xPlaza => $xDist) {
|
||||||
|
if ($ePlaza === $xPlaza) continue;
|
||||||
|
$price = lookupToll($tollIndex, $op, $ePlaza, $xPlaza);
|
||||||
|
if ($price !== null) {
|
||||||
|
$distSum = $eDist + $xDist;
|
||||||
|
if ($distSum < $bestDistSum) {
|
||||||
|
$bestDistSum = $distSum;
|
||||||
|
$bestToll = $price;
|
||||||
|
$bestEntry = $ePlaza;
|
||||||
|
$bestExit = $xPlaza;
|
||||||
|
$bestOp = $op;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($bestToll !== null) {
|
||||||
|
$segToll = $bestToll;
|
||||||
|
$entryPlaza = $bestEntry;
|
||||||
|
$exitPlaza = $bestExit;
|
||||||
|
} else {
|
||||||
|
$tollNote = 'péages non estimés pour ce trajet';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$tollNote = 'base GPS des péages non disponible';
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalToll += $segToll;
|
||||||
|
|
||||||
|
$seg = [
|
||||||
|
'from' => $fromName,
|
||||||
|
'to' => $toName,
|
||||||
|
'distance_km' => round($distKm, 1),
|
||||||
|
'toll' => round($segToll, 2),
|
||||||
|
];
|
||||||
|
if ($entryPlaza) $seg['entry_plaza'] = $entryPlaza;
|
||||||
|
if ($exitPlaza) $seg['exit_plaza'] = $exitPlaza;
|
||||||
|
if ($bestOp) $seg['op'] = $bestOp;
|
||||||
|
if ($tollNote) $seg['note'] = $tollNote;
|
||||||
|
|
||||||
|
$segments[] = $seg;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fuelCost = round(($totalKm / 100.0) * $fuelL100 * $fuelPrice, 2);
|
||||||
|
$grandTotal = round($totalToll + $fuelCost, 2);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => true,
|
||||||
|
'segments' => $segments,
|
||||||
|
'total_km' => round($totalKm, 1),
|
||||||
|
'total_toll' => round($totalToll, 2),
|
||||||
|
'fuel_cost' => $fuelCost,
|
||||||
|
'grand_total' => $grandTotal,
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+57
-6
@@ -38,6 +38,47 @@ function createFamilyDb(PDO $meta, string $db_host, string $db_user, string $db_
|
|||||||
return $db_name;
|
return $db_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateCalendarWeeks(PDO $familyPdo) {
|
||||||
|
$monthsFr = [1=>'Janvier', 2=>'Février', 3=>'Mars', 4=>'Avril', 5=>'Mai', 6=>'Juin', 7=>'Juillet', 8=>'Août', 9=>'Septembre', 10=>'Octobre', 11=>'Novembre', 12=>'Décembre'];
|
||||||
|
|
||||||
|
$sql = "REPLACE INTO pf_calendar_weeks
|
||||||
|
(year, week_iso_year, week_iso_number, week_label, month, month_name, week_start_date, mon_date, tue_date, wed_date, thu_date, fri_date)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||||
|
$stmt = $familyPdo->prepare($sql);
|
||||||
|
|
||||||
|
// On commence de l'année précédente jusqu'à +10 ans
|
||||||
|
$currentYear = (int)date('Y');
|
||||||
|
$startDate = new DateTime(($currentYear - 1) . '-08-28'); // On s'assure de couvrir la rentrée scolaire précédente
|
||||||
|
$endDate = new DateTime(($currentYear + 10) . '-12-31');
|
||||||
|
|
||||||
|
while ($startDate <= $endDate) {
|
||||||
|
$mon = clone $startDate;
|
||||||
|
$tue = clone $startDate; $tue->modify('+1 day');
|
||||||
|
$wed = clone $startDate; $wed->modify('+2 days');
|
||||||
|
$thu = clone $startDate; $thu->modify('+3 days');
|
||||||
|
$fri = clone $startDate; $fri->modify('+4 days');
|
||||||
|
|
||||||
|
$month = (int)$mon->format('n');
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
(int)$mon->format('Y'),
|
||||||
|
(int)$mon->format('o'),
|
||||||
|
(int)$mon->format('W'),
|
||||||
|
"Semaine " . (int)$mon->format('W'),
|
||||||
|
$month,
|
||||||
|
$monthsFr[$month],
|
||||||
|
$mon->format('Y-m-d'),
|
||||||
|
$mon->format('Y-m-d'),
|
||||||
|
$tue->format('Y-m-d'),
|
||||||
|
$wed->format('Y-m-d'),
|
||||||
|
$thu->format('Y-m-d'),
|
||||||
|
$fri->format('Y-m-d')
|
||||||
|
]);
|
||||||
|
|
||||||
|
$startDate->modify('+1 week');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Traitement du formulaire ─────────────────────────────────────────────────
|
// ─── Traitement du formulaire ─────────────────────────────────────────────────
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
@@ -90,13 +131,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
"INSERT INTO users (username, password_hash, display_name, family_id) VALUES (?, ?, ?, ?)"
|
"INSERT INTO users (username, password_hash, display_name, family_id) VALUES (?, ?, ?, ?)"
|
||||||
)->execute([$username, $hash, $display_name, $family['id']]);
|
)->execute([$username, $hash, $display_name, $family['id']]);
|
||||||
|
|
||||||
|
$user_id = (int)$meta_pdo->lastInsertId();
|
||||||
|
|
||||||
|
// 🔥 INJECTION ICI : Ajouter la personne à la base de la famille (Couleur orangée pour différencier)
|
||||||
|
$tenant_db = $family['db_name'];
|
||||||
|
$tenantPdo = new PDO("mysql:host=$db_host;dbname=$tenant_db;charset=utf8mb4", $db_user, $db_pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||||
|
$stmtPerson = $tenantPdo->prepare("INSERT INTO pf_people (name, user_id, role, color, is_active) VALUES (?, ?, 'parent', '#f59e0b', 1)");
|
||||||
|
$stmtPerson->execute([$display_name, $user_id]);
|
||||||
|
|
||||||
$_SESSION['user'] = [
|
$_SESSION['user'] = [
|
||||||
'id' => (int)$meta_pdo->lastInsertId(),
|
'id' => $user_id,
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
'display_name' => $display_name,
|
'display_name' => $display_name,
|
||||||
'family_id' => (int)$family['id'],
|
'family_id' => (int)$family['id'],
|
||||||
];
|
];
|
||||||
$_SESSION['family_db'] = $family['db_name'];
|
$_SESSION['family_db'] = $tenant_db;
|
||||||
session_regenerate_id(true);
|
session_regenerate_id(true);
|
||||||
header('Location: /index.php');
|
header('Location: /index.php');
|
||||||
exit;
|
exit;
|
||||||
@@ -129,6 +178,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
)->execute([$username, $hash, $display_name, $family_id]);
|
)->execute([$username, $hash, $display_name, $family_id]);
|
||||||
$user_id = (int)$meta_pdo->lastInsertId();
|
$user_id = (int)$meta_pdo->lastInsertId();
|
||||||
|
|
||||||
|
$tenantPdo = new PDO("mysql:host=$db_host;dbname=$db_name;charset=utf8mb4", $db_user, $db_pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||||
|
$stmtPerson = $tenantPdo->prepare("INSERT INTO pf_people (name, user_id, role, color, is_active) VALUES (?, ?, 'parent', '#0891b2', 1)");
|
||||||
|
$stmtPerson->execute([$display_name, $user_id]);
|
||||||
|
|
||||||
|
generateCalendarWeeks($tenantPdo);
|
||||||
|
|
||||||
$meta_pdo->commit();
|
$meta_pdo->commit();
|
||||||
|
|
||||||
$_SESSION['user'] = [
|
$_SESSION['user'] = [
|
||||||
@@ -169,7 +224,6 @@ require __DIR__ . '/header.php';
|
|||||||
<div class="pf-login-error"><?= htmlspecialchars($error) ?></div>
|
<div class="pf-login-error"><?= htmlspecialchars($error) ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- Onglets -->
|
|
||||||
<div style="display:flex;gap:8px;margin-bottom:24px">
|
<div style="display:flex;gap:8px;margin-bottom:24px">
|
||||||
<button id="tab-create" onclick="switchTab('create')"
|
<button id="tab-create" onclick="switchTab('create')"
|
||||||
class="pf-btn" style="flex:1">Nouvel espace</button>
|
class="pf-btn" style="flex:1">Nouvel espace</button>
|
||||||
@@ -177,7 +231,6 @@ require __DIR__ . '/header.php';
|
|||||||
class="pf-btn btn-secondary" style="flex:1">Rejoindre</button>
|
class="pf-btn btn-secondary" style="flex:1">Rejoindre</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Formulaire commun -->
|
|
||||||
<form method="post" action="/register.php" id="reg-form">
|
<form method="post" action="/register.php" id="reg-form">
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
||||||
<input type="hidden" name="action" id="reg-action" value="create">
|
<input type="hidden" name="action" id="reg-action" value="create">
|
||||||
@@ -208,7 +261,6 @@ require __DIR__ . '/header.php';
|
|||||||
placeholder="••••••" autocomplete="new-password">
|
placeholder="••••••" autocomplete="new-password">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Champs spécifiques : Nouvel espace -->
|
|
||||||
<div id="section-create">
|
<div id="section-create">
|
||||||
<div class="pf-form-group">
|
<div class="pf-form-group">
|
||||||
<label class="pf-label">Nom de la famille / foyer</label>
|
<label class="pf-label">Nom de la famille / foyer</label>
|
||||||
@@ -218,7 +270,6 @@ require __DIR__ . '/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Champs spécifiques : Rejoindre -->
|
|
||||||
<div id="section-join" style="display:none">
|
<div id="section-join" style="display:none">
|
||||||
<div class="pf-form-group">
|
<div class="pf-form-group">
|
||||||
<label class="pf-label">Code d'invitation</label>
|
<label class="pf-label">Code d'invitation</label>
|
||||||
|
|||||||
+264
-27
@@ -1,10 +1,18 @@
|
|||||||
-- HouseHub — Schéma MySQL
|
-- HouseHub — Schéma MySQL (Modèle pour une base famille)
|
||||||
-- Créer la base : CREATE DATABASE househub CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
SET NAMES utf8mb4;
|
SET NAMES utf8mb4;
|
||||||
SET FOREIGN_KEY_CHECKS = 0;
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
-- ─── Utilisateurs ─────────────────────────────────────────────────────────────
|
-- ─── Configuration du foyer ───────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_foyer_settings (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
currency VARCHAR(10) NOT NULL DEFAULT '€',
|
||||||
|
zone_scolaire VARCHAR(5) NOT NULL DEFAULT 'C',
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '€', 'C');
|
||||||
|
|
||||||
|
-- ─── Utilisateurs (Legacy) ────────────────────────────────────────────────────
|
||||||
CREATE TABLE IF NOT EXISTS pf_users (
|
CREATE TABLE IF NOT EXISTS pf_users (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
username VARCHAR(100) NOT NULL UNIQUE,
|
username VARCHAR(100) NOT NULL UNIQUE,
|
||||||
@@ -13,9 +21,13 @@ CREATE TABLE IF NOT EXISTS pf_users (
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- ─── Personnes ────────────────────────────────────────────────────────────────
|
-- ─── Personnes ────────────────────────────────────────────────────────────────
|
||||||
CREATE TABLE IF NOT EXISTS pf_people (
|
CREATE TABLE `pf_people` (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
name VARCHAR(100) NOT NULL
|
`name` varchar(100) NOT NULL,
|
||||||
|
`user_id` int(11) DEFAULT NULL,
|
||||||
|
`role` varchar(50) DEFAULT NULL,
|
||||||
|
`color` varchar(7) DEFAULT '#0891b2',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- ─── Calendrier familial ──────────────────────────────────────────────────────
|
-- ─── Calendrier familial ──────────────────────────────────────────────────────
|
||||||
@@ -91,7 +103,7 @@ CREATE TABLE IF NOT EXISTS pf_budget_items (
|
|||||||
CREATE TABLE IF NOT EXISTS pf_expenses (
|
CREATE TABLE IF NOT EXISTS pf_expenses (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
date_exp DATE NOT NULL,
|
date_exp DATE NOT NULL,
|
||||||
gestion_month VARCHAR(7) NOT NULL,
|
gestion_month VARCHAR(10) NOT NULL,
|
||||||
category VARCHAR(100),
|
category VARCHAR(100),
|
||||||
label VARCHAR(255),
|
label VARCHAR(255),
|
||||||
amount DECIMAL(10,2) DEFAULT 0,
|
amount DECIMAL(10,2) DEFAULT 0,
|
||||||
@@ -101,26 +113,28 @@ CREATE TABLE IF NOT EXISTS pf_expenses (
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_alloc_categories (
|
CREATE TABLE IF NOT EXISTS pf_alloc_categories (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
name VARCHAR(100) NOT NULL,
|
name VARCHAR(100) NOT NULL,
|
||||||
target DECIMAL(10,2) DEFAULT 0,
|
target DECIMAL(10,2) DEFAULT 0.00,
|
||||||
holiday_id INT DEFAULT NULL,
|
transfer_dest VARCHAR(50) DEFAULT NULL,
|
||||||
sort_order INT DEFAULT 0
|
holiday_id INT DEFAULT NULL,
|
||||||
|
sort_order INT DEFAULT 0
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_alloc_values (
|
CREATE TABLE IF NOT EXISTS pf_alloc_values (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
month_date VARCHAR(7) NOT NULL,
|
month_date VARCHAR(10) NOT NULL,
|
||||||
cat_id INT NOT NULL,
|
cat_id INT NOT NULL,
|
||||||
amount_alex DECIMAL(10,2) DEFAULT 0,
|
person_id INT NOT NULL,
|
||||||
amount_laia DECIMAL(10,2) DEFAULT 0,
|
amount DECIMAL(10,2) DEFAULT 0.00,
|
||||||
UNIQUE KEY uq_alloc (month_date, cat_id)
|
UNIQUE KEY uq_alloc_person (month_date, cat_id, person_id),
|
||||||
|
FOREIGN KEY (person_id) REFERENCES pf_people(id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_savings (
|
CREATE TABLE IF NOT EXISTS pf_savings (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
owner VARCHAR(50) NOT NULL,
|
owner VARCHAR(50) NOT NULL,
|
||||||
month_date VARCHAR(7) NOT NULL,
|
month_date VARCHAR(10) NOT NULL,
|
||||||
category VARCHAR(100) NOT NULL,
|
category VARCHAR(100) NOT NULL,
|
||||||
amount DECIMAL(10,2) DEFAULT 0,
|
amount DECIMAL(10,2) DEFAULT 0,
|
||||||
holiday_id INT DEFAULT NULL
|
holiday_id INT DEFAULT NULL
|
||||||
@@ -147,9 +161,23 @@ CREATE TABLE IF NOT EXISTS pf_salary_config (
|
|||||||
CREATE TABLE IF NOT EXISTS pf_import_rules (
|
CREATE TABLE IF NOT EXISTS pf_import_rules (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
keyword VARCHAR(255) NOT NULL UNIQUE,
|
keyword VARCHAR(255) NOT NULL UNIQUE,
|
||||||
category VARCHAR(100) NOT NULL
|
category VARCHAR(100) NOT NULL,
|
||||||
|
budget_item_id INT(11) NULL DEFAULT NULL
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `pf_advances` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`advance_date` date NOT NULL,
|
||||||
|
`payer` varchar(50) NOT NULL,
|
||||||
|
`description` varchar(255) NOT NULL,
|
||||||
|
`amount` decimal(10,2) DEFAULT 0.00,
|
||||||
|
`from_savings` tinyint(1) DEFAULT 0,
|
||||||
|
`is_resolved` tinyint(1) DEFAULT 0,
|
||||||
|
`created_at` datetime DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ─── Notes / Memo ─────────────────────────────────────────────────────────────
|
||||||
CREATE TABLE IF NOT EXISTS pf_notes (
|
CREATE TABLE IF NOT EXISTS pf_notes (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
note_type VARCHAR(100) NOT NULL,
|
note_type VARCHAR(100) NOT NULL,
|
||||||
@@ -158,6 +186,29 @@ CREATE TABLE IF NOT EXISTS pf_notes (
|
|||||||
UNIQUE KEY uq_note (note_type, reference_id)
|
UNIQUE KEY uq_note (note_type, reference_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_memo_notes (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
content LONGTEXT DEFAULT '',
|
||||||
|
tags VARCHAR(1000) DEFAULT '',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FULLTEXT KEY ft_notes (title, content, tags)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_memo_attachments (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
note_id INT NOT NULL,
|
||||||
|
type ENUM('image','file','url') NOT NULL DEFAULT 'file',
|
||||||
|
filename VARCHAR(255) DEFAULT NULL,
|
||||||
|
original_name VARCHAR(255) DEFAULT NULL,
|
||||||
|
url TEXT DEFAULT NULL,
|
||||||
|
label VARCHAR(255) DEFAULT NULL,
|
||||||
|
size INT DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (note_id) REFERENCES pf_memo_notes(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- ─── Voyages / Holidays ───────────────────────────────────────────────────────
|
-- ─── Voyages / Holidays ───────────────────────────────────────────────────────
|
||||||
CREATE TABLE IF NOT EXISTS pf_holidays (
|
CREATE TABLE IF NOT EXISTS pf_holidays (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@@ -225,16 +276,153 @@ CREATE TABLE IF NOT EXISTS pf_gifts (
|
|||||||
amount DECIMAL(8,2) DEFAULT 0
|
amount DECIMAL(8,2) DEFAULT 0
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
-- ─── Garage Manager ───────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_vehicles (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
brand VARCHAR(100) NOT NULL,
|
||||||
|
model VARCHAR(100) NOT NULL,
|
||||||
|
year INT DEFAULT NULL,
|
||||||
|
license_plate VARCHAR(50) DEFAULT NULL,
|
||||||
|
vin VARCHAR(100) DEFAULT NULL,
|
||||||
|
fuel_type VARCHAR(50) DEFAULT 'Essence',
|
||||||
|
color VARCHAR(50) DEFAULT NULL,
|
||||||
|
purchase_date DATE DEFAULT NULL,
|
||||||
|
purchase_price DECIMAL(10,2) DEFAULT NULL,
|
||||||
|
current_km INT DEFAULT 0,
|
||||||
|
photo VARCHAR(255) DEFAULT NULL,
|
||||||
|
notes TEXT DEFAULT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- ─── Données initiales ────────────────────────────────────────────────────────
|
CREATE TABLE IF NOT EXISTS pf_maintenances (
|
||||||
-- Utilisateur admin (mot de passe : changeme → à modifier !)
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
INSERT IGNORE INTO pf_users (id, username, password_hash, display_name)
|
vehicle_id INT NOT NULL,
|
||||||
VALUES (1, 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin');
|
type VARCHAR(100) NOT NULL,
|
||||||
|
description TEXT DEFAULT NULL,
|
||||||
|
date DATE NOT NULL,
|
||||||
|
km INT DEFAULT NULL,
|
||||||
|
cost DECIMAL(10,2) DEFAULT 0,
|
||||||
|
mechanic VARCHAR(100) DEFAULT NULL,
|
||||||
|
garage_name VARCHAR(100) DEFAULT NULL,
|
||||||
|
next_km INT DEFAULT NULL,
|
||||||
|
next_date DATE DEFAULT NULL,
|
||||||
|
invoice_photo VARCHAR(255) DEFAULT NULL,
|
||||||
|
notes TEXT DEFAULT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (vehicle_id) REFERENCES pf_vehicles(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- Personnes (IDs fixes correspondant aux constantes dans config.php)
|
CREATE TABLE IF NOT EXISTS pf_parts (
|
||||||
INSERT IGNORE INTO pf_people (id, name) VALUES (2, 'Alex');
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
INSERT IGNORE INTO pf_people (id, name) VALUES (3, 'Laia');
|
vehicle_id INT DEFAULT NULL,
|
||||||
|
maintenance_id INT DEFAULT NULL,
|
||||||
|
brand VARCHAR(100) DEFAULT NULL,
|
||||||
|
reference VARCHAR(100) DEFAULT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
category VARCHAR(100) DEFAULT 'Autre',
|
||||||
|
price DECIMAL(10,2) DEFAULT 0,
|
||||||
|
quantity INT DEFAULT 1,
|
||||||
|
unit VARCHAR(50) DEFAULT 'pièce',
|
||||||
|
supplier VARCHAR(100) DEFAULT NULL,
|
||||||
|
purchase_date DATE DEFAULT NULL,
|
||||||
|
photo VARCHAR(255) DEFAULT NULL,
|
||||||
|
notes TEXT DEFAULT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (vehicle_id) REFERENCES pf_vehicles(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (maintenance_id) REFERENCES pf_maintenances(id) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_garage_documents (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
vehicle_id INT NOT NULL,
|
||||||
|
maintenance_id INT DEFAULT NULL,
|
||||||
|
label VARCHAR(255) DEFAULT NULL,
|
||||||
|
filename VARCHAR(255) NOT NULL,
|
||||||
|
original_name VARCHAR(255) DEFAULT NULL,
|
||||||
|
mime VARCHAR(120) DEFAULT NULL,
|
||||||
|
size INT DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
KEY idx_gd_vehicle (vehicle_id),
|
||||||
|
KEY idx_gd_maint (maintenance_id),
|
||||||
|
FOREIGN KEY (vehicle_id) REFERENCES pf_vehicles(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (maintenance_id) REFERENCES pf_maintenances(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ─── Todo ─────────────────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_todo_lists (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
color VARCHAR(20) DEFAULT '#3b82f6',
|
||||||
|
icon VARCHAR(10) DEFAULT '📋',
|
||||||
|
position INT DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_todos (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
list_id INT DEFAULT NULL,
|
||||||
|
title VARCHAR(500) NOT NULL,
|
||||||
|
notes TEXT DEFAULT NULL,
|
||||||
|
due_date DATE DEFAULT NULL,
|
||||||
|
due_time TIME DEFAULT NULL,
|
||||||
|
notified TINYINT(1) DEFAULT 0,
|
||||||
|
notified_date DATE DEFAULT NULL,
|
||||||
|
priority ENUM('none','low','medium','high') DEFAULT 'none',
|
||||||
|
done TINYINT(1) DEFAULT 0,
|
||||||
|
done_at DATETIME DEFAULT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (list_id) REFERENCES pf_todo_lists(id) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ─── Module Liste (Courses partagées) ─────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_lists (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL DEFAULT 'Ma liste',
|
||||||
|
color VARCHAR(20) DEFAULT NULL,
|
||||||
|
list_type VARCHAR(50) DEFAULT NULL,
|
||||||
|
position INT NOT NULL DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO pf_lists (id, name, position) VALUES (1, 'Ma liste', 0);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_grocery_items (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
list_id INT NOT NULL DEFAULT 1,
|
||||||
|
category_id INT DEFAULT NULL,
|
||||||
|
label VARCHAR(500) NOT NULL,
|
||||||
|
in_cart TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
position INT NOT NULL DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
KEY idx_items_list (list_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_grocery_history (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
label_hash CHAR(64) NOT NULL,
|
||||||
|
label_display VARCHAR(500) NOT NULL,
|
||||||
|
last_used_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uq_grocery_hist_hash (label_hash),
|
||||||
|
KEY idx_hist_last (last_used_at)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_list_categories (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
icon VARCHAR(10) NOT NULL DEFAULT '🏷️',
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
position INT NOT NULL DEFAULT 0
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_item_category_rules (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
keyword VARCHAR(255) NOT NULL,
|
||||||
|
category_id INT NOT NULL,
|
||||||
|
UNIQUE KEY uq_keyword (keyword)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- ─── Calendar iOS ─────────────────────────────────────────────────────────────
|
-- ─── Calendar iOS ─────────────────────────────────────────────────────────────
|
||||||
CREATE TABLE IF NOT EXISTS pf_calendar_events (
|
CREATE TABLE IF NOT EXISTS pf_calendar_events (
|
||||||
@@ -264,8 +452,57 @@ CREATE TABLE IF NOT EXISTS pf_calendar_event_links (
|
|||||||
external_uid VARCHAR(255) NOT NULL,
|
external_uid VARCHAR(255) NOT NULL,
|
||||||
external_etag VARCHAR(255) DEFAULT NULL,
|
external_etag VARCHAR(255) DEFAULT NULL,
|
||||||
calendar_url VARCHAR(1024) DEFAULT NULL,
|
calendar_url VARCHAR(1024) DEFAULT NULL,
|
||||||
|
external_href VARCHAR(2048) DEFAULT NULL,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY uq_calendar_event (calendar_event_id),
|
UNIQUE KEY uq_calendar_event (calendar_event_id),
|
||||||
UNIQUE KEY uq_external_link (external_uid)
|
UNIQUE KEY uq_external_link (external_uid)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ─── PrintVault ───────────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_pv_models (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT DEFAULT '',
|
||||||
|
category VARCHAR(100) DEFAULT 'Non classé',
|
||||||
|
tags VARCHAR(500) DEFAULT '',
|
||||||
|
file_type VARCHAR(20) NOT NULL,
|
||||||
|
filename VARCHAR(255) NOT NULL,
|
||||||
|
original_name VARCHAR(255) NOT NULL,
|
||||||
|
file_size INT DEFAULT 0,
|
||||||
|
dim_x DECIMAL(10,3) DEFAULT 0,
|
||||||
|
dim_y DECIMAL(10,3) DEFAULT 0,
|
||||||
|
dim_z DECIMAL(10,3) DEFAULT 0,
|
||||||
|
volume DECIMAL(10,3) DEFAULT 0,
|
||||||
|
gcode_time VARCHAR(50) DEFAULT '',
|
||||||
|
gcode_filament VARCHAR(50) DEFAULT '',
|
||||||
|
gcode_nozzle VARCHAR(20) DEFAULT '',
|
||||||
|
gcode_bed VARCHAR(20) DEFAULT '',
|
||||||
|
thumb VARCHAR(255) DEFAULT '',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_pv_categories (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
color VARCHAR(20) DEFAULT '#8b5cf6'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO pf_pv_categories (name, color) VALUES
|
||||||
|
('Non classé','#64748b'),('Déco','#ec4899'),('Fonctionnel','#3b82f6'),
|
||||||
|
('Mécanique','#f59e0b'),('Jouets','#10b981'),('Outils','#ef4444'),
|
||||||
|
('Architecture','#8b5cf6'),('Art','#06b6d4');
|
||||||
|
|
||||||
|
-- ─── Planka ───────────────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_planka_config (
|
||||||
|
id INT NOT NULL DEFAULT 1,
|
||||||
|
project_id VARCHAR(32),
|
||||||
|
admin_token TEXT,
|
||||||
|
token_expires_at DATETIME,
|
||||||
|
active_board_id VARCHAR(32),
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
+527
-326
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user