gift dynamique
This commit is contained in:
+241
-73
@@ -8,51 +8,39 @@ 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 (Multi-tenant) ---
|
||||||
$baseAdults = ['Laia', 'Laura', 'Avi Iaia'];
|
$stmt = $pdo->query("SELECT name FROM pf_people WHERE role = 'enfant' AND is_active = 1 ORDER BY name ASC");
|
||||||
$extraAdults = ['Pauline', 'Papy JC', 'Mamy Caro'];
|
$children = $stmt->fetchAll(PDO::FETCH_COLUMN) ?: ['Aucun enfant configuré'];
|
||||||
|
|
||||||
$adultsByChildForAnniv = [
|
$stmt = $pdo->query("SELECT name FROM pf_people WHERE role NOT IN ('enfant', 'nounou') AND is_active = 1 ORDER BY name ASC");
|
||||||
'Pol' => array_merge($baseAdults, $extraAdults),
|
$allAdultsList = $stmt->fetchAll(PDO::FETCH_COLUMN) ?: ['Aucun adulte configuré'];
|
||||||
'Pep' => array_merge($baseAdults, $extraAdults),
|
|
||||||
'Elna' => $baseAdults,
|
|
||||||
'Bru' => $baseAdults,
|
|
||||||
'Guim' => $baseAdults,
|
|
||||||
];
|
|
||||||
|
|
||||||
$VIEWS = [
|
$stmt = $pdo->query("SELECT code, name, month_date FROM pf_gift_occasions WHERE is_active = 1 ORDER BY month_date ASC, id ASC");
|
||||||
'nadal' => ['TIO', 'NOEL', 'ROIS'],
|
$activeOccasions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
'anniversary' => ['ANNIV', 'SANT'],
|
|
||||||
];
|
|
||||||
|
|
||||||
$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', 'month_date' => null]];
|
||||||
$_SESSION['gift_view'] = $currentView;
|
}
|
||||||
$allowedOccasions = $VIEWS[$currentView];
|
$allowedOccasionCodes = array_column($activeOccasions, 'code');
|
||||||
|
|
||||||
$allOccasionLabels = [
|
|
||||||
'TIO' => tr('gift_occ_tio'), 'NOEL' => tr('gift_occ_noel'), 'ROIS' => tr('gift_occ_rois'),
|
|
||||||
'ANNIV' => tr('gift_occ_anniv'), 'SANT' => tr('gift_occ_sant')
|
|
||||||
];
|
|
||||||
$occasionIcons = [
|
$occasionIcons = [
|
||||||
'TIO' => '/modules/gift-list/assets/img/tio.png', 'NOEL' => '/modules/gift-list/assets/img/santa.png',
|
'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',
|
'ROIS' => '/modules/gift-list/assets/img/reis.png', 'ANNIV' => '/modules/gift-list/assets/img/corona.png',
|
||||||
'SANT' => '/modules/gift-list/assets/img/sant.png'
|
'SANT' => '/modules/gift-list/assets/img/sant.png'
|
||||||
];
|
];
|
||||||
|
|
||||||
// --- 2. DONNÉES ---
|
// --- 3. CHARGEMENT DES CADEAUX ---
|
||||||
$inMarks = implode(',', array_fill(0, count($allowedOccasions), '?'));
|
$inMarks = implode(',', array_fill(0, count($allowedOccasionCodes), '?'));
|
||||||
// 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 +50,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 +86,15 @@ 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>
|
<h1>🎁 <?= sprintf(tr('gift_main_title'), $year) ?></h1>
|
||||||
<div class="cl-view-switch">
|
<button class="btn btn-ghost btn-icon" id="btn-open-gift-settings" title="<?= tr('settings') ?>">⚙️</button>
|
||||||
<a href="?view=nadal" class="cl-view-btn <?= $currentView === 'nadal' ? 'is-active' : '' ?>"><?= tr('gift_view_nadal') ?></a>
|
|
||||||
<a href="?view=anniversary" class="cl-view-btn <?= $currentView === 'anniversary' ? 'is-active' : '' ?>"><?= tr('gift_view_anniv') ?></a>
|
|
||||||
</div>
|
|
||||||
</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 +128,22 @@ 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'];
|
||||||
|
$occName = $occ['name'];
|
||||||
|
?>
|
||||||
<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; ?>
|
<?php if(!empty($occasionIcons[$occCode])): ?>
|
||||||
<?= $allOccasionLabels[$occCode] ?>
|
<img src="<?= $occasionIcons[$occCode] ?>" class="cl-occasion-icon" alt="">
|
||||||
|
<?php else: ?>
|
||||||
|
<span>🎀</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?= 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 +151,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 +163,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 +196,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') ?? 'Bilan & Remboursements' ?></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') ?> à <?= 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; ?>
|
||||||
@@ -247,10 +243,10 @@ require __DIR__ . '/header.php';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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,11 +270,42 @@ require __DIR__ . '/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. Modale Paramètres Fêtes -->
|
||||||
|
<div class="gift-settings-backdrop" id="modal-gift-settings">
|
||||||
|
<div class="gift-settings-modal">
|
||||||
|
<div class="gift-settings-header">
|
||||||
|
<h3>⚙️ <?= tr('gift_settings_title') ?? 'Configuration' ?></h3>
|
||||||
|
<button class="gift-settings-close" id="btn-close-gift-settings">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="gift-settings-body">
|
||||||
|
|
||||||
|
<h4 class="gift-add-subtitle"><?= tr('gift_settings_add_title') ?? '+ Ajouter' ?></h4>
|
||||||
|
<form id="form-add-occasion" class="gift-add-form">
|
||||||
|
<input type="text" class="pf-input" name="name" placeholder="<?= tr('gift_settings_name_placeholder') ?? 'Nom' ?>" required style="flex: 1; min-width: 150px; padding: 0.4rem 0.75rem;">
|
||||||
|
<input type="text" class="pf-input" name="month_date" placeholder="<?= tr('gift_settings_date_placeholder') ?? 'MM-JJ' ?>" style="width: 120px; padding: 0.4rem 0.75rem;">
|
||||||
|
<button type="submit" class="btn btn-secondary"><?= tr('btn_add') ?? 'Ajouter' ?></button>
|
||||||
|
</form>
|
||||||
|
<small class="gift-help-text"><?= tr('gift_settings_date_help') ?? 'Optionnel' ?></small>
|
||||||
|
|
||||||
|
<hr style="border:0; border-bottom:1px solid var(--border-light); margin: 1rem 0;">
|
||||||
|
|
||||||
|
<form id="form-save-toggles">
|
||||||
|
<div id="occasions-list-container" class="gift-occasions-list"></div>
|
||||||
|
|
||||||
|
<div class="modal-footer gift-modal-custom-footer" style="padding-top: 1rem; border-top: 1px solid var(--border-light);">
|
||||||
|
<button type="button" class="btn btn-secondary" id="btn-cancel-settings"><?= tr('btn_cancel') ?? 'Annuler' ?></button>
|
||||||
|
<button type="submit" class="btn btn-primary"><?= tr('btn_save') ?? 'Enregistrer' ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 1. GESTION DU COMPOSANT MULTI-SELECT VANILLA
|
// 1. GESTION DU COMPOSANT MULTI-SELECT VANILLA
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
function toggleMS(listId, triggerEl) {
|
function toggleMS(listId, triggerEl) {
|
||||||
document.querySelectorAll('.pf-ms-dropdown').forEach(el => { if (el.id !== listId) el.classList.remove('open'); });
|
document.querySelectorAll('.pf-ms-dropdown').forEach(el => { if (el.id !== listId) el.classList.remove('open'); });
|
||||||
document.querySelectorAll('.pf-ms-trigger').forEach(el => { if (el !== triggerEl) el.classList.remove('active'); });
|
document.querySelectorAll('.pf-ms-trigger').forEach(el => { if (el !== triggerEl) el.classList.remove('active'); });
|
||||||
@@ -381,14 +408,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 +434,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 +443,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 +474,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 +505,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 +525,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 +544,159 @@ if (giftForm) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// GESTION DES PARAMÈTRES (MODALE FÊTES ⚙️)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
// 1. Fonctions d'ouverture / fermeture propres
|
||||||
|
function closeGiftSettings() {
|
||||||
|
document.getElementById('modal-gift-settings').classList.remove('show');
|
||||||
|
document.body.classList.remove('no-scroll');
|
||||||
|
}
|
||||||
|
|
||||||
|
const btnOpenSettings = document.getElementById('btn-open-gift-settings');
|
||||||
|
if (btnOpenSettings) {
|
||||||
|
btnOpenSettings.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const modalSettings = document.getElementById('modal-gift-settings');
|
||||||
|
if (modalSettings) {
|
||||||
|
modalSettings.classList.add('show');
|
||||||
|
document.body.classList.add('no-scroll');
|
||||||
|
await loadGiftOccasions();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btn-close-gift-settings')?.addEventListener('click', closeGiftSettings);
|
||||||
|
document.getElementById('btn-cancel-settings')?.addEventListener('click', closeGiftSettings);
|
||||||
|
|
||||||
|
// Fermeture au clic sur le fond (backdrop)
|
||||||
|
const modalGiftSettings = document.getElementById('modal-gift-settings');
|
||||||
|
if (modalGiftSettings) {
|
||||||
|
modalGiftSettings.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modalGiftSettings) {
|
||||||
|
closeGiftSettings();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Chargement dynamique de la liste
|
||||||
|
async function loadGiftOccasions() {
|
||||||
|
const container = document.getElementById('occasions-list-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const txtLoading = window.I18N && window.I18N['loading'] ? window.I18N['loading'] : '⏳';
|
||||||
|
const txtEmpty = window.I18N && window.I18N['gift_settings_empty'] ? window.I18N['gift_settings_empty'] : 'Vide';
|
||||||
|
const txtActive = window.I18N && window.I18N['gift_settings_active'] ? window.I18N['gift_settings_active'] : 'Actif';
|
||||||
|
|
||||||
|
container.innerHTML = `<div class="gift-loader">${txtLoading}</div>`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/modules/gift-list/api-settings.php?action=get_occasions');
|
||||||
|
const text = await res.text();
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
|
||||||
|
if (!data.ok) throw new Error(data.error);
|
||||||
|
|
||||||
|
if (data.data.length === 0) {
|
||||||
|
container.innerHTML = `<em>${txtEmpty}</em>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '<div class="gift-occasions-grid">';
|
||||||
|
data.data.forEach(occ => {
|
||||||
|
const isChecked = occ.is_active == 1 ? 'checked' : '';
|
||||||
|
const dateBadge = occ.month_date ? `<span class="gift-date-badge">${occ.month_date}</span>` : '';
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="pf-card gift-occasion-card">
|
||||||
|
<div>
|
||||||
|
<strong>${occ.name}</strong> ${dateBadge}
|
||||||
|
</div>
|
||||||
|
<label class="gift-occasion-toggle">
|
||||||
|
<input type="checkbox" class="occ-toggle-cb" data-id="${occ.id}" ${isChecked}>
|
||||||
|
<small>${txtActive}</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
container.innerHTML = `<div class="gift-error-msg">Erreur : ${e.message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Soumission du formulaire d'AJOUT
|
||||||
|
const formAddOccasion = document.getElementById('form-add-occasion');
|
||||||
|
if (formAddOccasion) {
|
||||||
|
formAddOccasion.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = formAddOccasion.querySelector('button[type="submit"]');
|
||||||
|
const oldText = btn.innerText;
|
||||||
|
btn.innerText = '⏳';
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
const formData = new FormData(formAddOccasion);
|
||||||
|
formData.append('action', 'add_occasion');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/modules/gift-list/api-settings.php', { method: 'POST', body: formData });
|
||||||
|
const text = await res.text();
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
|
||||||
|
if (!data.ok) throw new Error(data.error || 'Erreur');
|
||||||
|
|
||||||
|
formAddOccasion.reset();
|
||||||
|
await loadGiftOccasions(); // Rafraîchit juste la liste dans la modale !
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
alert("Erreur: " + err.message);
|
||||||
|
} finally {
|
||||||
|
btn.innerText = oldText;
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Soumission GLOBALE (Bouton Enregistrer) pour les Checkboxes
|
||||||
|
const formSaveToggles = document.getElementById('form-save-toggles');
|
||||||
|
if (formSaveToggles) {
|
||||||
|
formSaveToggles.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = formSaveToggles.querySelector('button[type="submit"]');
|
||||||
|
const oldText = btn.innerText;
|
||||||
|
btn.innerText = '⏳';
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
// On récolte l'état de toutes les checkboxes
|
||||||
|
const states = [];
|
||||||
|
formSaveToggles.querySelectorAll('.occ-toggle-cb').forEach(cb => {
|
||||||
|
states.push({ id: cb.dataset.id, state: cb.checked ? 1 : 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', 'save_toggles');
|
||||||
|
formData.append('states', JSON.stringify(states));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/modules/gift-list/api-settings.php', { method: 'POST', body: formData });
|
||||||
|
const text = await res.text();
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
|
||||||
|
if (!data.ok) throw new Error(data.error);
|
||||||
|
|
||||||
|
// Succès : on rafraîchit la page pour afficher/masquer les onglets
|
||||||
|
window.location.reload();
|
||||||
|
} catch (err) {
|
||||||
|
alert("Erreur: " + err.message);
|
||||||
|
btn.innerText = oldText;
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<?php require __DIR__ . '/footer.php'; ?>
|
<?php require __DIR__ . '/footer.php'; ?>
|
||||||
+48
-38
@@ -602,47 +602,57 @@ return [
|
|||||||
'bud_adv_confirm_delete' => 'Eliminar definitivament aquest avançament?',
|
'bud_adv_confirm_delete' => 'Eliminar definitivament aquest avançament?',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// CADEAUX (GIFTS)
|
// MÒDUL: REGALS (gift-list)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'gift_page_title' => 'HouseHub - Llista de regals',
|
|
||||||
'gift_occ_tio' => 'Tió',
|
// 1. Títols i capçaleres
|
||||||
'gift_occ_noel' => 'Nadal',
|
'gift_page_title' => 'Llista de Regals',
|
||||||
'gift_occ_rois' => 'Reis',
|
'gift_main_title' => 'Regals de %s', // %s serà substituït per l'any (ex: Regals de 2026)
|
||||||
'gift_occ_anniv' => 'Aniversari',
|
'settings' => 'Configuració',
|
||||||
'gift_occ_sant' => 'Sant',
|
|
||||||
'gift_main_title' => 'Llista de regals %s',
|
// 2. Filtres
|
||||||
'gift_aria_change_view' => 'Canvia la vista',
|
|
||||||
'gift_view_nadal' => 'Nadal',
|
|
||||||
'gift_view_anniv' => 'Aniversaris',
|
|
||||||
'gift_view_by_party' => 'Vista per festa',
|
|
||||||
'gift_no_gifts' => 'No hi ha cap regal registrat per a %s en aquesta vista.',
|
|
||||||
'gift_add_gift' => 'Afegeix un regal',
|
|
||||||
'gift_paid_by' => 'pagat per %s',
|
|
||||||
'gift_summary_title' => 'Resum del pressupost',
|
|
||||||
'gift_col_adult' => 'Adult',
|
|
||||||
'gift_col_child' => 'Infant',
|
|
||||||
'gift_col_party' => 'Festa',
|
|
||||||
'gift_debtor' => 'Deutor',
|
|
||||||
'gift_creditor' => 'Creditor',
|
|
||||||
'gift_liquidations' => 'Tricount',
|
|
||||||
'gift_no_debt' => 'Cap deute pendent.',
|
|
||||||
'gift_owes' => 'ha de pagar',
|
|
||||||
'gift_to' => 'a',
|
|
||||||
'gift_detailed_list' => 'Llista detallada de regals',
|
|
||||||
'gift_col_gift' => 'Regal',
|
|
||||||
'gift_col_link' => 'Enllaç',
|
|
||||||
'gift_modal_title_add' => 'Afegeix un regal per a %s',
|
|
||||||
'gift_modal_title_edit' => 'Edita el regal',
|
|
||||||
'gift_modal_payer' => 'Pagat per',
|
|
||||||
'gift_modal_gift_name' => 'Nom del regal',
|
|
||||||
'gift_modal_ph_name' => 'p. ex., Lego Star Wars',
|
|
||||||
'gift_modal_price' => 'Preu (€)',
|
|
||||||
'gift_modal_link' => 'Enllaç (opcional)',
|
|
||||||
'gift_confirm_delete' => 'Vols eliminar aquest regal?',
|
|
||||||
'gift_filter_all_children' => 'Tots els nens',
|
'gift_filter_all_children' => 'Tots els nens',
|
||||||
'gift_filter_all_adults' => 'Tots els adults',
|
'gift_filter_all_adults' => 'Tots els adults',
|
||||||
'gift_empty_state_no_gifts' => 'Cap regal de moment.',
|
|
||||||
'gift_empty_state_no_filter' => 'Cap regal correspon al filtre.',
|
// 3. Contingut i estats buits
|
||||||
|
'gift_add_gift' => 'Afegir',
|
||||||
|
'gift_empty_state_no_gifts' => 'Cap regal registrat per a aquesta ocasió.',
|
||||||
|
'gift_empty_state_no_filter' => 'Cap regal coincideix amb els teus filtres.',
|
||||||
|
'gift_paid_by' => 'Pagat per %s', // %s serà substituït pel nom del pagador
|
||||||
|
|
||||||
|
// 4. Secció Tricount (Balanç i Reemborsaments)
|
||||||
|
'gift_liquidations' => 'Balanç i Reemborsaments',
|
||||||
|
'gift_no_debt' => 'Tot està al dia, cap deute!',
|
||||||
|
'gift_owes' => 'deu',
|
||||||
|
'gift_view_matrix' => 'Veure la matriu detallada',
|
||||||
|
'gift_debtor' => 'Deutor',
|
||||||
|
'gift_creditor' => 'Creditor',
|
||||||
|
|
||||||
|
// 5. Modal: Afegir/Modificar Regal
|
||||||
|
'gift_col_adult' => 'Per compte de',
|
||||||
|
'gift_modal_payer' => 'Pagat per',
|
||||||
|
'gift_modal_gift_name' => 'Descripció del regal',
|
||||||
|
'gift_modal_price' => 'Preu estimat / pagat (€)',
|
||||||
|
'gift_modal_link' => 'Enllaç del producte (URL)',
|
||||||
|
'gift_modal_title_add' => 'Afegir per a %s', // JS: %s = nom del nen
|
||||||
|
'gift_modal_title_edit' => 'Modificar el regal', // JS
|
||||||
|
'gift_confirm_delete' => 'Vols eliminar realment aquest regal?', // JS
|
||||||
|
|
||||||
|
// 6. Modal: Configuració de les Festes
|
||||||
|
'gift_settings_title' => 'Configuració de les Festes',
|
||||||
|
'gift_settings_add_title' => '+ Afegir una ocasió',
|
||||||
|
'gift_settings_name_placeholder' => 'Nom (ex: Sant Valentí)',
|
||||||
|
'gift_settings_date_placeholder' => 'MM-DD',
|
||||||
|
'gift_settings_date_help' => 'La data (MM-DD) és opcional. Deixa en blanc per a una festa mòbil.',
|
||||||
|
'gift_settings_empty' => 'Cap festa configurada.', // JS
|
||||||
|
'gift_settings_active' => 'Actiu', // JS
|
||||||
|
|
||||||
|
// 7. Botons genèrics
|
||||||
|
'btn_cancel' => 'Cancel·lar',
|
||||||
|
'btn_save' => 'Desar',
|
||||||
|
'btn_add' => 'Afegir',
|
||||||
|
'loading' => 'Carregant...', //
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// GARAGE MANAGER
|
// GARAGE MANAGER
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
+46
-38
@@ -592,48 +592,56 @@ return [
|
|||||||
'btn_save' => 'Save',
|
'btn_save' => 'Save',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// GIFTS
|
// MODULE: GIFTS (gift-list)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'gift_page_title' => 'HouseHub - Gift list',
|
|
||||||
'gift_occ_tio' => 'Tió',
|
// 1. Titles and headers
|
||||||
'gift_occ_noel' => 'Christmas',
|
'gift_page_title' => 'Gift List',
|
||||||
'gift_occ_rois' => 'Three Kings',
|
'gift_main_title' => 'Gifts of %s', // %s will be replaced by the year (e.g., Gifts of 2026)
|
||||||
'gift_occ_anniv' => 'Birthday',
|
'settings' => 'Settings',
|
||||||
'gift_occ_sant' => 'Name day',
|
|
||||||
'gift_main_title' => 'Gift list %s',
|
// 2. Filters
|
||||||
'gift_aria_change_view' => 'Change view',
|
|
||||||
'gift_view_nadal' => 'Christmas',
|
|
||||||
'gift_view_anniv' => 'Birthdays',
|
|
||||||
'gift_view_by_party' => 'View by occasion',
|
|
||||||
'gift_no_gifts' => 'No gifts recorded for %s in this view.',
|
|
||||||
'gift_add_gift' => 'Add a gift',
|
|
||||||
'gift_paid_by' => 'paid by %s',
|
|
||||||
'gift_summary_title' => 'Budget summary',
|
|
||||||
'gift_col_adult' => 'Adult',
|
|
||||||
'gift_col_child' => 'Child',
|
|
||||||
'gift_col_party' => 'Occasion',
|
|
||||||
'gift_debtor' => 'Debtor',
|
|
||||||
'gift_creditor' => 'Creditor',
|
|
||||||
'gift_liquidations' => 'Settlement',
|
|
||||||
'gift_no_debt' => 'No outstanding debts.',
|
|
||||||
'gift_owes' => 'owes',
|
|
||||||
'gift_to' => 'to',
|
|
||||||
'gift_detailed_list' => 'Detailed gift list',
|
|
||||||
'gift_col_gift' => 'Gift',
|
|
||||||
'gift_col_link' => 'Link',
|
|
||||||
'gift_modal_title_add' => 'Add a gift for %s',
|
|
||||||
'gift_modal_title_edit' => 'Edit gift',
|
|
||||||
'gift_modal_payer' => 'Paid by',
|
|
||||||
'gift_modal_gift_name' => 'Gift name',
|
|
||||||
'gift_modal_ph_name' => 'e.g. Lego Star Wars',
|
|
||||||
'gift_modal_price' => 'Price (€)',
|
|
||||||
'gift_modal_link' => 'Link (optional)',
|
|
||||||
'gift_confirm_delete' => 'Are you sure you want to delete this gift?',
|
|
||||||
'gift_filter_all_children' => 'All children',
|
'gift_filter_all_children' => 'All children',
|
||||||
'gift_filter_all_adults' => 'All adults',
|
'gift_filter_all_adults' => 'All adults',
|
||||||
'gift_empty_state_no_gifts' => 'No gifts yet.',
|
|
||||||
'gift_empty_state_no_filter' => 'No gifts match the filter.',
|
// 3. Content and empty states
|
||||||
|
'gift_add_gift' => 'Add',
|
||||||
|
'gift_empty_state_no_gifts' => 'No gifts recorded for this occasion.',
|
||||||
|
'gift_empty_state_no_filter' => 'No gifts match your filters.',
|
||||||
|
'gift_paid_by' => 'Paid by %s', // %s will be replaced by the payer's name
|
||||||
|
|
||||||
|
// 4. Tricount Section (Balance & Refunds)
|
||||||
|
'gift_liquidations' => 'Balance & Refunds',
|
||||||
|
'gift_no_debt' => 'Everything is settled, no debts!',
|
||||||
|
'gift_owes' => 'owes',
|
||||||
'gift_view_matrix' => 'View detailed matrix',
|
'gift_view_matrix' => 'View detailed matrix',
|
||||||
|
'gift_debtor' => 'Debtor',
|
||||||
|
'gift_creditor' => 'Creditor',
|
||||||
|
|
||||||
|
// 5. Modal: Add/Edit Gift
|
||||||
|
'gift_col_adult' => 'On behalf of',
|
||||||
|
'gift_modal_payer' => 'Paid by',
|
||||||
|
'gift_modal_gift_name' => 'Gift description',
|
||||||
|
'gift_modal_price' => 'Estimated / paid price (€)',
|
||||||
|
'gift_modal_link' => 'Product link (URL)',
|
||||||
|
'gift_modal_title_add' => 'Add for %s', // JS: %s = child's name
|
||||||
|
'gift_modal_title_edit' => 'Edit gift', // JS
|
||||||
|
'gift_confirm_delete' => 'Are you sure you want to delete this gift?', // JS
|
||||||
|
|
||||||
|
// 6. Modal: Occasions Settings
|
||||||
|
'gift_settings_title' => 'Occasions Settings',
|
||||||
|
'gift_settings_add_title' => '+ Add an occasion',
|
||||||
|
'gift_settings_name_placeholder' => 'Name (e.g. Valentine\'s Day)',
|
||||||
|
'gift_settings_date_placeholder' => 'MM-DD',
|
||||||
|
'gift_settings_date_help' => 'Date (MM-DD) is optional. Leave empty for a floating date.',
|
||||||
|
'gift_settings_empty' => 'No occasion configured.', // JS
|
||||||
|
'gift_settings_active' => 'Active', // JS
|
||||||
|
|
||||||
|
// 7. Generic buttons
|
||||||
|
'btn_cancel' => 'Cancel',
|
||||||
|
'btn_save' => 'Save',
|
||||||
|
'btn_add' => 'Add',
|
||||||
|
'loading' => 'Loading...',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// GARAGE MANAGER
|
// GARAGE MANAGER
|
||||||
|
|||||||
+47
-38
@@ -599,48 +599,57 @@ return [
|
|||||||
'bud_adv_confirm_delete' => 'Supprimer définitivement cette avance ?',
|
'bud_adv_confirm_delete' => 'Supprimer définitivement cette avance ?',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// CADEAUX (GIFTS)
|
// MODULE : CADEAUX (gift-list)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
'gift_page_title' => 'HouseHub - Liste de cadeaux',
|
|
||||||
'gift_occ_tio' => 'Tió',
|
// 1. Titres et en-têtes
|
||||||
'gift_occ_noel' => 'Noël',
|
'gift_page_title' => 'Liste des Cadeaux',
|
||||||
'gift_occ_rois' => 'Rois mages',
|
'gift_main_title' => 'Cadeaux de %s', // %s sera remplacé par l'année (ex: Cadeaux de 2026)
|
||||||
'gift_occ_anniv' => 'Anniversaire',
|
'settings' => 'Paramètres',
|
||||||
'gift_occ_sant' => 'Saint',
|
|
||||||
'gift_main_title' => 'Liste de cadeaux %s',
|
// 2. Filtres
|
||||||
'gift_aria_change_view' => 'Changer la vue',
|
|
||||||
'gift_view_nadal' => 'Noël',
|
|
||||||
'gift_view_anniv' => 'Anniversaires',
|
|
||||||
'gift_view_by_party' => 'Vue par fête',
|
|
||||||
'gift_no_gifts' => 'Aucun cadeau enregistré pour %s dans cette vue.',
|
|
||||||
'gift_add_gift' => 'Ajouter un cadeau',
|
|
||||||
'gift_paid_by' => 'payé par %s',
|
|
||||||
'gift_summary_title' => 'Résumé du budget',
|
|
||||||
'gift_col_adult' => 'Adulte',
|
|
||||||
'gift_col_child' => 'Enfant',
|
|
||||||
'gift_col_party' => 'Fête',
|
|
||||||
'gift_debtor' => 'Débiteur',
|
|
||||||
'gift_creditor' => 'Créancier',
|
|
||||||
'gift_liquidations' => 'Tricount',
|
|
||||||
'gift_no_debt' => 'Aucune dette en cours.',
|
|
||||||
'gift_owes' => 'doit',
|
|
||||||
'gift_to' => 'à',
|
|
||||||
'gift_detailed_list' => 'Liste détaillée des cadeaux',
|
|
||||||
'gift_col_gift' => 'Cadeau',
|
|
||||||
'gift_col_link' => 'Lien',
|
|
||||||
'gift_modal_title_add' => 'Ajouter un cadeau pour %s',
|
|
||||||
'gift_modal_title_edit' => 'Modifier le cadeau',
|
|
||||||
'gift_modal_payer' => 'Payé par',
|
|
||||||
'gift_modal_gift_name' => 'Nom du cadeau',
|
|
||||||
'gift_modal_ph_name' => 'ex: Lego Star Wars',
|
|
||||||
'gift_modal_price' => 'Prix (€)',
|
|
||||||
'gift_modal_link' => 'Lien (optionnel)',
|
|
||||||
'gift_confirm_delete' => 'Voulez-vous vraiment supprimer ce cadeau ?',
|
|
||||||
'gift_filter_all_children' => 'Tous les enfants',
|
'gift_filter_all_children' => 'Tous les enfants',
|
||||||
'gift_filter_all_adults' => 'Tous les adultes',
|
'gift_filter_all_adults' => 'Tous les adultes',
|
||||||
'gift_empty_state_no_gifts' => 'Aucun cadeau pour le moment.',
|
|
||||||
'gift_empty_state_no_filter' => 'Aucun cadeau ne correspond au filtre.',
|
// 3. Contenu et états vides
|
||||||
|
'gift_add_gift' => 'Ajouter',
|
||||||
|
'gift_empty_state_no_gifts' => 'Aucun cadeau enregistré pour cette fête.',
|
||||||
|
'gift_empty_state_no_filter' => 'Aucun cadeau ne correspond à vos filtres.',
|
||||||
|
'gift_paid_by' => 'Payé par %s', // %s sera remplacé par le nom du payeur
|
||||||
|
|
||||||
|
// 4. Section Tricount (Bilan & Remboursements)
|
||||||
|
'gift_liquidations' => 'Bilan & Remboursements',
|
||||||
|
'gift_no_debt' => 'Tout est à jour, aucune dette !',
|
||||||
|
'gift_owes' => 'doit',
|
||||||
'gift_view_matrix' => 'Voir la matrice détaillée',
|
'gift_view_matrix' => 'Voir la matrice détaillée',
|
||||||
|
'gift_debtor' => 'Débiteur',
|
||||||
|
'gift_creditor' => 'Créancier',
|
||||||
|
|
||||||
|
// 5. Modale : Ajout/Modification de Cadeau
|
||||||
|
'gift_col_adult' => 'Pour le compte de',
|
||||||
|
'gift_modal_payer' => 'Payé par',
|
||||||
|
'gift_modal_gift_name' => 'Description du cadeau',
|
||||||
|
'gift_modal_price' => 'Prix estimé / payé (€)',
|
||||||
|
'gift_modal_link' => 'Lien du produit (URL)',
|
||||||
|
'gift_modal_title_add' => 'Ajouter pour %s', // JS : %s = nom de l'enfant
|
||||||
|
'gift_modal_title_edit' => 'Modifier le cadeau', // JS
|
||||||
|
'gift_confirm_delete' => 'Voulez-vous vraiment supprimer ce cadeau ?', // JS
|
||||||
|
|
||||||
|
// 6. Modale : Paramètres des Fêtes (Nouveau !)
|
||||||
|
'gift_settings_title' => 'Configuration des Fêtes',
|
||||||
|
'gift_settings_add_title' => '+ Ajouter une occasion',
|
||||||
|
'gift_settings_name_placeholder' => 'Nom (ex: Saint Valentin)',
|
||||||
|
'gift_settings_date_placeholder' => 'MM-JJ',
|
||||||
|
'gift_settings_date_help' => 'La date (MM-JJ) est optionnelle. Laissez vide pour une fête mobile.',
|
||||||
|
'gift_settings_empty' => 'Aucune fête configurée.', // JS
|
||||||
|
'gift_settings_active' => 'Actif', // JS
|
||||||
|
|
||||||
|
// 7. Boutons génériques (si tu ne les as pas déjà en global)
|
||||||
|
'btn_cancel' => 'Annuler',
|
||||||
|
'btn_save' => 'Enregistrer',
|
||||||
|
'btn_add' => 'Ajouter',
|
||||||
|
'loading' => 'Chargement...',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// GARAGE MANAGER
|
// GARAGE MANAGER
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
+136
-239
@@ -1,261 +1,158 @@
|
|||||||
<?php
|
<?php
|
||||||
// Script de migration : Refonte globale Multi-Tenant & Synchronisation des schémas
|
/**
|
||||||
|
* Script de migration global HouseHub OS (Multi-tenant)
|
||||||
|
* À exécuter via le navigateur : http://localhost:8083/migrate.php
|
||||||
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/includes/meta_db.php';
|
require_once __DIR__ . '/includes/meta_db.php';
|
||||||
|
|
||||||
echo "<h1>🚀 Début de la migration Globale ...</h1>";
|
$db_host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||||
|
$db_user = getenv('DB_USER') ?: 'househub';
|
||||||
|
$db_pass = getenv('DB_PASS') ?: 'househub_dev';
|
||||||
|
|
||||||
|
echo "<h1>🚀 Début de la migration Multi-Tenant</h1>";
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 0. MISE À JOUR DE LA META DB
|
|
||||||
// ==========================================
|
|
||||||
echo "<h3>Mise à jour Meta DB (househub_meta)</h3><ul>";
|
|
||||||
try {
|
try {
|
||||||
$meta_pdo->exec("ALTER TABLE user_calendar_integrations ADD COLUMN calendar_prefs_json TEXT DEFAULT NULL");
|
$stmt = $meta_pdo->query("SELECT db_name, name FROM families WHERE is_active = 1");
|
||||||
echo "<li><span style='color:green'>Colonne 'calendar_prefs_json' ajoutée à user_calendar_integrations.</span></li>";
|
$families = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
} catch (\PDOException $e) {
|
|
||||||
if ($e->getCode() == '42S21' || strpos($e->getMessage(), '1060') !== false) {
|
foreach ($families as $family) {
|
||||||
echo "<li><span style='color:gray'>Colonne 'calendar_prefs_json' déjà présente.</span></li>";
|
$dbName = $family['db_name'];
|
||||||
} else { throw $e; }
|
echo "<h2>Famille : {$family['name']} ($dbName)</h2>";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = new PDO(
|
||||||
|
"mysql:host=$db_host;dbname=$dbName;charset=utf8mb4",
|
||||||
|
$db_user, $db_pass,
|
||||||
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// 1. UPDATE DE pf_people (Juste la date de naissance et l'état actif)
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
$colCheck = $pdo->prepare("SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'pf_people' AND COLUMN_NAME = 'birthdate'");
|
||||||
|
$colCheck->execute([$dbName]);
|
||||||
|
if ($colCheck->rowCount() === 0) {
|
||||||
|
$pdo->exec("ALTER TABLE pf_people
|
||||||
|
ADD COLUMN birthdate DATE DEFAULT NULL,
|
||||||
|
ADD COLUMN is_active TINYINT(1) DEFAULT 1
|
||||||
|
");
|
||||||
|
echo "✅ pf_people mis à jour (birthdate ajouté).<br>";
|
||||||
|
} else {
|
||||||
|
echo "➖ pf_people déjà à jour.<br>";
|
||||||
}
|
}
|
||||||
echo "</ul>";
|
|
||||||
|
|
||||||
// ==========================================
|
// ---------------------------------------------------------
|
||||||
// MIGRATIONS PAR FAMILLE
|
// 2. CRÉATION DES NOUVELLES TABLES (IF NOT EXISTS)
|
||||||
// ==========================================
|
// ---------------------------------------------------------
|
||||||
$stmt = $meta_pdo->query("SELECT id, name, db_name FROM families WHERE db_name != ''");
|
|
||||||
$families = $stmt->fetchAll();
|
|
||||||
|
|
||||||
$host = getenv('DB_HOST') ?: 'househub-db';
|
// Comptes bancaires
|
||||||
$user = getenv('DB_USER') ?: 'househub';
|
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_bank_accounts (
|
||||||
$pass = getenv('DB_PASS') ?: 'changeme';
|
|
||||||
|
|
||||||
foreach ($families as $f) {
|
|
||||||
$db_name = $f['db_name'];
|
|
||||||
$family_id = $f['id'];
|
|
||||||
echo "<h3>Mise à jour de <strong>{$f['name']}</strong> ($db_name)</h3><ul>";
|
|
||||||
|
|
||||||
$p1_name = null;
|
|
||||||
$p2_name = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
$fam_pdo = new PDO("mysql:host=$host;dbname=$db_name;charset=utf8mb4", $user, $pass, [
|
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 1. Table pf_foyer_settings
|
|
||||||
$fam_pdo->exec("CREATE TABLE IF NOT EXISTS pf_foyer_settings (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
currency VARCHAR(10) NOT NULL DEFAULT '€',
|
name VARCHAR(100) NOT NULL,
|
||||||
zone_scolaire VARCHAR(5) NOT NULL DEFAULT 'C',
|
owner_person_id INT DEFAULT NULL,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
account_type VARCHAR(50) DEFAULT 'savings',
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
is_default TINYINT(1) DEFAULT 0,
|
||||||
$fam_pdo->exec("INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '€', 'C');");
|
FOREIGN KEY (owner_person_id) REFERENCES pf_people(id) ON DELETE SET NULL
|
||||||
echo "<li><span style='color:green'>Table pf_foyer_settings vérifiée/créée.</span></li>";
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||||
|
|
||||||
// 2. Colonne external_href pour le Calendrier iOS
|
// Catégories de budget
|
||||||
try {
|
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_budget_categories (
|
||||||
$fam_pdo->exec("ALTER TABLE pf_calendar_event_links ADD COLUMN external_href VARCHAR(2048) DEFAULT NULL");
|
|
||||||
echo "<li><span style='color:green'>Colonne 'external_href' ajoutée à pf_calendar_event_links.</span></li>";
|
|
||||||
} catch (\PDOException $e) {
|
|
||||||
if ($e->getCode() != '42S21' && strpos($e->getMessage(), '1060') === false) throw $e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Colonne budget_item_id pour les règles d'import
|
|
||||||
try {
|
|
||||||
$fam_pdo->exec("ALTER TABLE pf_import_rules ADD COLUMN budget_item_id INT(11) DEFAULT NULL");
|
|
||||||
echo "<li><span style='color:green'>Colonne 'budget_item_id' ajoutée à pf_import_rules.</span></li>";
|
|
||||||
} catch (\PDOException $e) {
|
|
||||||
if ($e->getCode() != '42S21' && strpos($e->getMessage(), '1060') === false) throw $e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Uniformisation des VARCHAR de dates pour le budget (YYYY-MM-01 = 10 chars)
|
|
||||||
$fam_pdo->exec("ALTER TABLE pf_expenses MODIFY gestion_month VARCHAR(10) NOT NULL");
|
|
||||||
$fam_pdo->exec("ALTER TABLE pf_alloc_values MODIFY month_date VARCHAR(10) NOT NULL");
|
|
||||||
$fam_pdo->exec("ALTER TABLE pf_savings MODIFY month_date VARCHAR(10) NOT NULL");
|
|
||||||
echo "<li><span style='color:green'>Formats de dates (VARCHAR 10) uniformisés pour le budget.</span></li>";
|
|
||||||
|
|
||||||
// 5. GESTION DE PF_PEOPLE (user_id, role, color)
|
|
||||||
echo "<li><strong>Mise à jour pf_people :</strong> ";
|
|
||||||
try { $fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN user_id INT NULL DEFAULT NULL"); } catch (\Exception $e) {}
|
|
||||||
try { $fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN role VARCHAR(50) DEFAULT NULL"); } catch (\Exception $e) {}
|
|
||||||
try { $fam_pdo->exec("ALTER TABLE pf_people ADD COLUMN color VARCHAR(7) DEFAULT '#0891b2'"); } catch (\Exception $e) {}
|
|
||||||
|
|
||||||
$stmtUsers = $meta_pdo->prepare("SELECT id, username, display_name FROM users WHERE family_id = ? ORDER BY id ASC");
|
|
||||||
$stmtUsers->execute([$family_id]);
|
|
||||||
$users = $stmtUsers->fetchAll();
|
|
||||||
|
|
||||||
foreach ($users as $u) {
|
|
||||||
$stmtCheck = $fam_pdo->prepare("SELECT id FROM pf_people WHERE user_id = ? OR LOWER(name) = LOWER(?)");
|
|
||||||
$stmtCheck->execute([$u['id'], $u['username']]);
|
|
||||||
$exists = $stmtCheck->fetchColumn();
|
|
||||||
|
|
||||||
if ($exists) {
|
|
||||||
$fam_pdo->prepare("UPDATE pf_people SET user_id = ? WHERE id = ?")->execute([$u['id'], $exists]);
|
|
||||||
} else {
|
|
||||||
$fam_pdo->prepare("INSERT INTO pf_people (name, user_id) VALUES (?, ?)")->execute([$u['display_name'] ?: $u['username'], $u['id']]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$fam_pdo->exec("UPDATE pf_people SET role = 'parent' WHERE user_id IS NOT NULL AND role IS NULL;");
|
|
||||||
$fam_pdo->exec("UPDATE pf_people SET role = 'nounou' WHERE LOWER(name) = 'carole';");
|
|
||||||
|
|
||||||
$pIds = $fam_pdo->query("SELECT id FROM pf_people WHERE role = 'parent' ORDER BY id ASC")->fetchAll(PDO::FETCH_COLUMN);
|
|
||||||
if (isset($pIds[0])) $fam_pdo->exec("UPDATE pf_people SET color = '#0891b2' WHERE id = " . (int)$pIds[0] . " AND color IS NULL");
|
|
||||||
if (isset($pIds[1])) $fam_pdo->exec("UPDATE pf_people SET color = '#f59e0b' WHERE id = " . (int)$pIds[1] . " AND color IS NULL");
|
|
||||||
echo "<span style='color:blue'>OK</span></li>";
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 6. REFONTE RELATIONNELLE : pf_alloc_values (Colonnes -> Lignes)
|
|
||||||
// ==========================================
|
|
||||||
echo "<li><strong>Table pf_alloc_values (Normalisation) :</strong> ";
|
|
||||||
|
|
||||||
// Vérifier si la table est déjà convertie
|
|
||||||
$checkNewFormat = $fam_pdo->query("SHOW COLUMNS FROM pf_alloc_values LIKE 'person_id'")->rowCount();
|
|
||||||
|
|
||||||
if ($checkNewFormat > 0) {
|
|
||||||
echo "<span style='color:gray'>Déjà convertie au format relationnel.</span></li>";
|
|
||||||
} else {
|
|
||||||
// A. Récupérer l'ordre des parents réels pour faire le mapping d'index
|
|
||||||
$stmtParents = $fam_pdo->query("SELECT id, name FROM pf_people WHERE role = 'parent' ORDER BY id ASC");
|
|
||||||
$orderedParents = $stmtParents->fetchAll();
|
|
||||||
|
|
||||||
// B. Sauvegarder les anciennes données à migrer
|
|
||||||
$oldAllocations = $fam_pdo->query("SELECT * FROM pf_alloc_values")->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
// C. Supprimer l'ancienne table
|
|
||||||
$fam_pdo->exec("DROP TABLE IF EXISTS pf_alloc_values");
|
|
||||||
|
|
||||||
// D. Créer la nouvelle table normalisée
|
|
||||||
$fam_pdo->exec("CREATE TABLE pf_alloc_values (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
month_date VARCHAR(10) NOT NULL,
|
code VARCHAR(50) NOT NULL,
|
||||||
cat_id INT NOT NULL,
|
label VARCHAR(100) NOT NULL,
|
||||||
person_id INT NOT NULL,
|
type VARCHAR(50) NOT NULL,
|
||||||
amount DECIMAL(10,2) DEFAULT 0.00,
|
color VARCHAR(20) DEFAULT '#ccc',
|
||||||
UNIQUE KEY uq_alloc_person (month_date, cat_id, person_id),
|
icon VARCHAR(20) DEFAULT '💰',
|
||||||
FOREIGN KEY (person_id) REFERENCES pf_people(id) ON DELETE CASCADE
|
UNIQUE KEY (code)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||||
|
|
||||||
// E. Migration des données
|
// Types de congés
|
||||||
$stmtInsert = $fam_pdo->prepare("INSERT INTO pf_alloc_values (month_date, cat_id, person_id, amount) VALUES (?, ?, ?, ?)");
|
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_leave_types (
|
||||||
$migratedRows = 0;
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
code VARCHAR(20) NOT NULL,
|
||||||
|
label VARCHAR(100) NOT NULL,
|
||||||
|
default_allowance DECIMAL(5,2) DEFAULT 0,
|
||||||
|
reset_month INT DEFAULT 1,
|
||||||
|
allow_carry_over TINYINT(1) DEFAULT 0,
|
||||||
|
UNIQUE KEY (code)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||||
|
|
||||||
foreach ($oldAllocations as $oldRow) {
|
// Fêtes (Cadeaux)
|
||||||
// Mapping des anciennes colonnes potentielles vers les index de parents
|
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_gift_occasions (
|
||||||
$possibleColumns = [
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
0 => ['amount_p1', 'amount_alex'],
|
code VARCHAR(20) NOT NULL,
|
||||||
1 => ['amount_p2', 'amount_laia'],
|
name VARCHAR(100) NOT NULL,
|
||||||
2 => ['amount_p3'],
|
month_date VARCHAR(5) DEFAULT NULL,
|
||||||
3 => ['amount_p4']
|
is_active TINYINT(1) DEFAULT 1,
|
||||||
];
|
UNIQUE KEY (code)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||||
|
|
||||||
foreach ($possibleColumns as $parentIndex => $colNames) {
|
// Règles Cadeaux (Qui paie pour quel enfant à quelle occasion)
|
||||||
if (!isset($orderedParents[$parentIndex])) continue;
|
// L'ERREUR DE SYNTAXE ÉTAIT ICI : UNIQUE KEY adult_child_occ
|
||||||
$targetParentId = $orderedParents[$parentIndex]['id'];
|
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_gift_rules (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
adult_person_id INT NOT NULL,
|
||||||
|
child_person_id INT NOT NULL,
|
||||||
|
occasion_id INT NOT NULL,
|
||||||
|
FOREIGN KEY (adult_person_id) REFERENCES pf_people(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (child_person_id) REFERENCES pf_people(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (occasion_id) REFERENCES pf_gift_occasions(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY adult_child_occ (adult_person_id, child_person_id, occasion_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||||
|
|
||||||
foreach ($colNames as $col) {
|
echo "✅ Nouvelles tables de configuration créées ou vérifiées.<br>";
|
||||||
if (isset($oldRow[$col]) && (float)$oldRow[$col] > 0) {
|
|
||||||
$stmtInsert->execute([
|
|
||||||
$oldRow['month_date'],
|
|
||||||
$oldRow['cat_id'],
|
|
||||||
$targetParentId,
|
|
||||||
(float)$oldRow[$col]
|
|
||||||
]);
|
|
||||||
$migratedRows++;
|
|
||||||
break; // Passer à l'index parent suivant dès qu'on a trouvé une valeur
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
echo "<span style='color:green'>Succès ! Nouvelle table créée, $migratedRows lignes migrées.</span></li>";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. GESTION DU BUDGET (pf_alloc_categories & pf_salary_config)
|
// ---------------------------------------------------------
|
||||||
$fam_pdo->exec("UPDATE pf_alloc_categories SET name = 'Eco P1' WHERE name LIKE 'Eco Alex%'");
|
// 3. INJECTION DE DONNÉES PAR DÉFAUT (Pour ne rien casser)
|
||||||
$fam_pdo->exec("UPDATE pf_alloc_categories SET name = 'Eco P2' WHERE name LIKE 'Eco Laia%'");
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
$stmtParents = $fam_pdo->query("SELECT name FROM pf_people WHERE role = 'parent' ORDER BY id ASC");
|
if ($pdo->query("SELECT COUNT(*) FROM pf_bank_accounts")->fetchColumn() == 0) {
|
||||||
$parents = $stmtParents->fetchAll();
|
$pdo->exec("INSERT INTO pf_bank_accounts (name, account_type, is_default) VALUES
|
||||||
if (count($parents) >= 2) {
|
('Compte Commun', 'checking', 1),
|
||||||
$fam_pdo->prepare("UPDATE pf_salary_config SET person = ? WHERE person = 'Alex'")->execute([$parents[0]['name']]);
|
('Livret A Alex', 'savings', 0),
|
||||||
$fam_pdo->prepare("UPDATE pf_salary_config SET person = ? WHERE person = 'Laia'")->execute([$parents[1]['name']]);
|
('Livret A Laia', 'savings', 0),
|
||||||
}
|
('Livret A Pol', 'savings', 0),
|
||||||
|
('Livret A Pep', 'savings', 0)
|
||||||
} catch (\PDOException $e) {
|
|
||||||
echo "<li style='color:red'>❌ Erreur : " . $e->getMessage() . "</li>";
|
|
||||||
}
|
|
||||||
echo "</ul>";
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 8. SEPARATION CIBLE BUDGET / DESTINATION VIREMENT
|
|
||||||
// ==========================================
|
|
||||||
echo "<li><strong>Table pf_alloc_categories (Fix Collision) :</strong> ";
|
|
||||||
try {
|
|
||||||
$fam_pdo->exec("ALTER TABLE pf_alloc_categories ADD COLUMN transfer_dest VARCHAR(50) DEFAULT NULL AFTER target");
|
|
||||||
echo "<span style='color:green'>Succès ! Colonne 'transfer_dest' ajoutée pour préserver vos objectifs chiffrés.</span></li>";
|
|
||||||
} catch (\PDOException $e) {
|
|
||||||
echo "<span style='color:gray'>La colonne transfer_dest existe déjà.</span></li>";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 9. TRANSFERT DES DONNÉES (Cible -> Destination)
|
|
||||||
// ==========================================
|
|
||||||
echo "<li><strong>Table pf_alloc_categories (Récupération des données) :</strong> ";
|
|
||||||
try {
|
|
||||||
// Vérifier si la colonne target est encore au format texte (VARCHAR)
|
|
||||||
$stmtCol = $fam_pdo->query("SHOW COLUMNS FROM pf_alloc_categories LIKE 'target'");
|
|
||||||
$colInfo = $stmtCol->fetch(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
if ($colInfo && strpos(strtolower($colInfo['Type']), 'varchar') !== false) {
|
|
||||||
// 1. Déplacer les textes "vers..."
|
|
||||||
$updated1 = $fam_pdo->exec("UPDATE pf_alloc_categories SET transfer_dest = target, target = '0' WHERE target LIKE 'vers %'");
|
|
||||||
|
|
||||||
// 2. Gérer le mot 'SYSTEM'
|
|
||||||
$updated2 = $fam_pdo->exec("UPDATE pf_alloc_categories SET transfer_dest = 'SYSTEM', target = '0' WHERE target = 'SYSTEM'");
|
|
||||||
|
|
||||||
// 3. Sécurité absolue avant conversion
|
|
||||||
$fam_pdo->exec("UPDATE pf_alloc_categories SET target = '0' WHERE target NOT REGEXP '^[0-9]+(\\\\.[0-9]+)?$'");
|
|
||||||
|
|
||||||
echo "<span style='color:green'>" . ($updated1 + $updated2) . " destinations récupérées et déplacées.</span><br>";
|
|
||||||
|
|
||||||
// 4. Remettre la colonne en format numérique
|
|
||||||
$fam_pdo->exec("ALTER TABLE pf_alloc_categories MODIFY target DECIMAL(10,2) DEFAULT 0.00");
|
|
||||||
echo "<span style='color:green'>Colonne 'target' re-sécurisée en format monétaire DECIMAL(10,2).</span></li>";
|
|
||||||
} else {
|
|
||||||
echo "<span style='color:gray'>La colonne 'target' est déjà au format DECIMAL, transfert ignoré.</span></li>";
|
|
||||||
}
|
|
||||||
} catch (\PDOException $e) {
|
|
||||||
echo "<span style='color:red'>Erreur : " . $e->getMessage() . "</span></li>";
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Récupération de toutes les bases de données de familles via la base meta
|
|
||||||
$stmtFamilies = $meta_pdo->query("SELECT id, db_name FROM families WHERE is_active = 1");
|
|
||||||
while ($fam = $stmtFamilies->fetch(PDO::FETCH_ASSOC)) {
|
|
||||||
error_log("Migration de la base familiale : " . $fam['db_name']);
|
|
||||||
|
|
||||||
$fam_dsn = "mysql:host=" . DB_HOST . ";dbname=" . $fam['db_name'] . ";charset=utf8mb4";
|
|
||||||
$fam_pdo = new PDO($fam_dsn, DB_USER, DB_PASS, $options);
|
|
||||||
|
|
||||||
// Injection incrémentale sécurisée
|
|
||||||
$fam_pdo->exec("
|
|
||||||
CREATE TABLE IF NOT EXISTS `pf_advances` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`advance_date` DATE NOT NULL,
|
|
||||||
`payer` VARCHAR(100) NOT NULL,
|
|
||||||
`description` VARCHAR(255) NOT NULL,
|
|
||||||
`amount` DECIMAL(10,2) DEFAULT 0.00,
|
|
||||||
`from_savings` TINYINT(1) DEFAULT 0,
|
|
||||||
`is_resolved` TINYINT(1) DEFAULT 0,
|
|
||||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
");
|
");
|
||||||
}
|
}
|
||||||
error_log("✅ Migration de la table pf_advances terminée sur tous les tenants.");
|
|
||||||
|
if ($pdo->query("SELECT COUNT(*) FROM pf_budget_categories")->fetchColumn() == 0) {
|
||||||
|
$pdo->exec("INSERT INTO pf_budget_categories (code, label, type, color, icon) VALUES
|
||||||
|
('INCOME', 'Revenus', 'Income', '#22c55e', '💵'),
|
||||||
|
('FMCG', 'Alimentation', 'Expense', '#3b82f6', '🛒'),
|
||||||
|
('FUEL', 'Carburant', 'Expense', '#f59e0b', '⛽'),
|
||||||
|
('SCHOOL', 'École / Garde', 'Expense', '#a855f7', '🎒'),
|
||||||
|
('HEALTH', 'Santé', 'Expense', '#ef4444', '⚕️')
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pdo->query("SELECT COUNT(*) FROM pf_leave_types")->fetchColumn() == 0) {
|
||||||
|
$pdo->exec("INSERT INTO pf_leave_types (code, label, default_allowance, reset_month, allow_carry_over) VALUES
|
||||||
|
('CA', 'Congés Annuels', 25, 6, 1),
|
||||||
|
('JRA', 'Jours de Repos', 10, 1, 0),
|
||||||
|
('JA', 'Jour Anniversaire', 1, 1, 0)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pdo->query("SELECT COUNT(*) FROM pf_gift_occasions")->fetchColumn() == 0) {
|
||||||
|
$pdo->exec("INSERT INTO pf_gift_occasions (code, name, month_date) VALUES
|
||||||
|
('NOEL', 'Noël', '12-25'),
|
||||||
|
('ROIS', 'Les Rois', '01-06'),
|
||||||
|
('ANNIV', 'Anniversaire', NULL)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "✅ Données de base injectées (si nécessaire).<br>";
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "❌ Erreur sur la base $dbName : " . $e->getMessage() . "<br>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "<h1>🎉 Migration terminée avec succès !</h1>";
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
die("❌ Erreur critique lors de la migration : " . $e->getMessage());
|
die("❌ Erreur fatale Meta DB : " . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
echo "<h2>🎉 Migration terminée avec succès !</h2>";
|
|
||||||
?>
|
?>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../../includes/db.php';
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$action = $_POST['action'] ?? $_GET['action'] ?? '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Lire toutes les fêtes
|
||||||
|
if ($action === 'get_occasions') {
|
||||||
|
$stmt = $pdo->query("SELECT * FROM pf_gift_occasions ORDER BY month_date ASC, id ASC");
|
||||||
|
echo json_encode(['ok' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Ajouter une nouvelle fête (Renvoie l'ID pour rafraîchir la modale)
|
||||||
|
if ($action === 'add_occasion') {
|
||||||
|
$name = trim($_POST['name'] ?? '');
|
||||||
|
$month_date = trim($_POST['month_date'] ?? '');
|
||||||
|
|
||||||
|
if (empty($name)) throw new Exception("Name required");
|
||||||
|
|
||||||
|
$code = strtoupper(preg_replace('/[^A-Za-z0-9]/', '', $name));
|
||||||
|
$code = substr($code, 0, 20);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pf_gift_occasions (code, name, month_date, is_active) VALUES (?, ?, ?, 1)");
|
||||||
|
$stmt->execute([$code, $name, $month_date ?: null]);
|
||||||
|
|
||||||
|
echo json_encode(['ok' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Sauvegarde globale des cases à cocher
|
||||||
|
if ($action === 'save_toggles') {
|
||||||
|
$states = json_decode($_POST['states'] ?? '[]', true);
|
||||||
|
if (is_array($states)) {
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_gift_occasions SET is_active = ? WHERE id = ?");
|
||||||
|
foreach ($states as $item) {
|
||||||
|
$stmt->execute([(int)$item['state'], (int)$item['id']]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo json_encode(['ok' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception("Action inconnue.");
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
+385
-55
@@ -64,7 +64,7 @@
|
|||||||
|
|
||||||
.pf-filter-select {
|
.pf-filter-select {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
border-radius: 20px; /* Forme de pillule */
|
border-radius: 20px;
|
||||||
border: 1px solid var(--border-light);
|
border: 1px solid var(--border-light);
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
@@ -75,16 +75,15 @@
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
width: auto; /* Empêche de prendre toute la largeur */
|
width: auto;
|
||||||
appearance: none; /* Nettoie la flèche système... */
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
-moz-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-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-repeat: no-repeat;
|
||||||
background-position: right 10px center;
|
background-position: right 10px center;
|
||||||
background-size: 12px;
|
background-size: 12px;
|
||||||
padding-right: 28px; /* Place pour la flèche */
|
padding-right: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pf-filter-select:focus {
|
.pf-filter-select:focus {
|
||||||
@@ -121,10 +120,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 +181,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 +189,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;
|
||||||
@@ -242,9 +227,8 @@
|
|||||||
border-right: 2px solid var(--border-light);
|
border-right: 2px solid var(--border-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Icônes des fêtes (Tió, Noël, etc.) plus petites et bien alignées */
|
|
||||||
.cl-occasion-icon {
|
.cl-occasion-icon {
|
||||||
width: 20px; /* Réduit pour être discret */
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
@@ -253,41 +237,14 @@
|
|||||||
.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 */
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 +267,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 +288,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 +328,380 @@
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header & Settings Button */
|
||||||
|
.gift-settings-btn {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty States */
|
||||||
|
.gift-empty-state {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-style: italic;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section Tricount (Bilan) */
|
||||||
|
.gift-tricount-section {
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
.gift-tricount-title {
|
||||||
|
margin-top: 0;
|
||||||
|
color: var(--text-main);
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
.gift-tricount-success {
|
||||||
|
color: var(--success);
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.gift-tricount-debt-amount {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
.gift-matrix-details {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.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,
|
||||||
|
.cl-debt-matrix td.sticky-col {
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
border-right: 2px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
.cl-debt-matrix th.sticky-col.bg-header {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
.cl-debt-matrix th.sticky-col.bg-body {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-cell-danger {
|
||||||
|
color: var(--danger);
|
||||||
|
font-weight: 700;
|
||||||
|
background: #fef2f2;
|
||||||
|
}
|
||||||
|
.gift-cell-muted {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modale Paramètres Occasions (Bottom Sheet style) */
|
||||||
|
.gift-settings-backdrop {
|
||||||
|
display: none;
|
||||||
|
align-items: flex-end; /* Bottom sheet on mobile */
|
||||||
|
}
|
||||||
|
.gift-settings-modal {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.gift-settings-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.gift-settings-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.gift-settings-body {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.gift-occasions-list {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.gift-occasions-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.gift-occasion-card {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.8rem 1rem;
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.gift-occasion-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.gift-date-badge {
|
||||||
|
font-size: 0.8em;
|
||||||
|
background: var(--bg-page);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.gift-add-subtitle {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.gift-add-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.gift-add-input-name {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
.gift-add-input-date {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
.gift-help-text {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
.gift-loader {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.gift-error-msg {
|
||||||
|
color: var(--danger);
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
/* ==========================================================================
|
||||||
|
BOUTONS & MODALE (Design System Todo)
|
||||||
|
========================================================================== */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.4rem 0.85rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: all 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.btn-icon {
|
||||||
|
padding: 0.25rem;
|
||||||
|
min-width: 28px;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
.btn-ghost {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.btn-ghost:hover {
|
||||||
|
color: var(--text-main);
|
||||||
|
background: var(--bg-page);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Correction de la surimpression de la modale Paramètres */
|
||||||
|
.gift-settings-backdrop {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 200;
|
||||||
|
background: rgba(15, 23, 42, 0.5);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-settings-modal {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 14px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 90vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
|
||||||
|
animation: modalIn 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.96) translateY(-8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
MODALE PARAMÈTRES (Design System Todo)
|
||||||
|
========================================================================== */
|
||||||
|
.gift-settings-backdrop {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 200;
|
||||||
|
background: rgba(15, 23, 42, 0.5);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.gift-settings-backdrop.show {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-settings-modal {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 14px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 90vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
|
||||||
|
animation: giftModalIn 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes giftModalIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.96) translateY(-8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-settings-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
.gift-settings-header h3 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.gift-settings-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
}
|
||||||
|
.gift-settings-close:hover {
|
||||||
|
background: var(--bg-page);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-settings-body {
|
||||||
|
padding: 1.25rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-add-subtitle {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
}
|
||||||
|
.gift-add-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
/* Aligner le bouton sur celui de Todo */
|
||||||
|
.gift-add-form .btn-primary {
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
.gift-add-form .btn-primary:hover {
|
||||||
|
background: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user