@@ -1268,3 +1268,223 @@ body.sum-mode-active .sum-target {
|
||||
padding: 12px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
MODALE PARAMÈTRES BUDGET (Design System HouseHub)
|
||||
========================================================================== */
|
||||
.budget-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;
|
||||
}
|
||||
.budget-settings-backdrop.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.budget-settings-modal {
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 14px;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
height: 80vh;
|
||||
max-height: 700px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
|
||||
animation: modalIn 0.18s ease;
|
||||
}
|
||||
|
||||
.bs-header {
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.bs-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-main);
|
||||
}
|
||||
.bs-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
border-radius: 6px;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
.bs-close:hover {
|
||||
background: var(--bg-page);
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.bs-layout {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
.bs-sidebar {
|
||||
width: 200px;
|
||||
border-right: 1px solid var(--border-light);
|
||||
background: var(--bg-page);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.75rem;
|
||||
gap: 0.4rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.bs-tab-btn {
|
||||
text-align: left;
|
||||
padding: 0.6rem 1rem;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: 0.15s;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.bs-tab-btn:hover {
|
||||
background: var(--border-light);
|
||||
color: var(--text-main);
|
||||
}
|
||||
.bs-tab-btn.active {
|
||||
background: var(--text-main);
|
||||
color: var(--bg-panel);
|
||||
}
|
||||
|
||||
.bs-content {
|
||||
flex: 1;
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
background: var(--bg-panel);
|
||||
}
|
||||
.bs-pane {
|
||||
display: none;
|
||||
}
|
||||
.bs-pane.active {
|
||||
display: block;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile Responsive */
|
||||
@media (max-width: 650px) {
|
||||
.bs-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
.bs-sidebar {
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
overflow-x: auto;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
padding: 0.5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bs-tab-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.bs-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Utilitaires pour les listes de paramètres */
|
||||
.bs-list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.bs-section-title {
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-main);
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* --- AFFICHAGE SMART DES RÈGLES (TAG CLOUD) --- */
|
||||
.bs-rule-search {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-light);
|
||||
background: var(--bg-page);
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-main);
|
||||
font-family: inherit;
|
||||
}
|
||||
.bs-rule-group {
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
.bs-rule-group-title {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
.bs-rule-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.bs-rule-tag {
|
||||
background: var(--bg-page);
|
||||
border: 1px solid var(--border-light);
|
||||
padding: 3px 10px;
|
||||
border-radius: 14px;
|
||||
font-size: 0.8rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: 0.15s;
|
||||
}
|
||||
.bs-rule-tag:hover {
|
||||
border-color: var(--danger);
|
||||
background: #fef2f2;
|
||||
}
|
||||
.bs-rule-tag-del {
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.bs-rule-tag:hover .bs-rule-tag-del {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
require __DIR__ . '/../../../../includes/auth.php';
|
||||
require __DIR__ . '/../../../../includes/db.php';
|
||||
require_login();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
$action = $_GET['action'] ?? $_POST['action'] ?? '';
|
||||
|
||||
try {
|
||||
if ($action === 'get_all') {
|
||||
// 1. Comptes bancaires avec le nom du propriétaire lié (pf_people)
|
||||
$accounts = $pdo->query("
|
||||
SELECT a.*, p.name as owner_name
|
||||
FROM pf_bank_accounts a
|
||||
LEFT JOIN pf_people p ON a.owner_person_id = p.id
|
||||
ORDER BY a.is_default DESC, a.name ASC
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 2. Catégories
|
||||
$categories = $pdo->query("SELECT * FROM pf_budget_categories ORDER BY type ASC, label ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 3. Règles d'import (avec le nom de la catégorie correspondante)
|
||||
// Utilisation de COLLATE pour éviter l'erreur 1267 de mix de collations
|
||||
$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 * FROM pf_salary_config WHERE year = $currentYear ORDER BY person ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Récupération de la devise du foyer (par défaut € si non définie)
|
||||
// Lecture directe de la colonne currency dans pf_foyer_settings
|
||||
$currencySetting = $pdo->query("SELECT currency FROM pf_foyer_settings LIMIT 1")->fetchColumn() ?: '€';
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'accounts' => $accounts,
|
||||
'categories' => $categories,
|
||||
'rules' => $rules,
|
||||
'salaries' => $salaries,
|
||||
'year' => $currentYear,
|
||||
'currency' => $currencySetting
|
||||
]
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- GESTION DES COMPTES BANCAIRES ---
|
||||
|
||||
if ($action === 'add_account') {
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$type = $_POST['type'] ?? 'checking';
|
||||
// On met is_default à 0 par défaut pour les nouveaux comptes
|
||||
|
||||
if (empty($name)) throw new Exception("Le nom du compte est obligatoire.");
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_bank_accounts (name, account_type, is_default) VALUES (?, ?, 0)");
|
||||
$stmt->execute([$name, $type]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete_account') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
|
||||
// Sécurité : on empêche de supprimer un compte s'il n'en reste qu'un seul
|
||||
$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.");
|
||||
|
||||
// Vérification anti-doublon sur le code
|
||||
$check = $pdo->prepare("SELECT COUNT(*) FROM pf_budget_categories WHERE code = ?");
|
||||
$check->execute([$code]);
|
||||
if ($check->fetchColumn() > 0) throw new Exception("Ce code de catégorie existe déjà.");
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_budget_categories (code, label, type, color, icon) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$code, $label, $type, $color, $icon]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete_category') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$stmt = $pdo->prepare("DELETE FROM pf_budget_categories WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- GESTION DES RÈGLES D'IMPORT ---
|
||||
|
||||
if ($action === 'add_rule') {
|
||||
$keyword = strtoupper(trim($_POST['keyword'] ?? ''));
|
||||
$category = trim($_POST['category'] ?? ''); // Le code de la catégorie (ex: FMCG)
|
||||
|
||||
if (empty($keyword) || empty($category)) throw new Exception("Le mot-clé et la catégorie sont obligatoires.");
|
||||
|
||||
// Vérification anti-doublon
|
||||
$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.");
|
||||
|
||||
// Mise à jour de la ligne pour l'année en cours
|
||||
$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'] ?? '€');
|
||||
|
||||
// Mise à jour directe de la colonne currency pour le foyer
|
||||
$stmt = $pdo->prepare("UPDATE pf_foyer_settings SET currency = ?");
|
||||
$stmt->execute([$currency]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
throw new Exception("Action API inconnue.");
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
@@ -955,3 +955,39 @@ td.col-laia-sub {
|
||||
gap: 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
MODALE PARAMÈTRES (SIDEBAR STYLE)
|
||||
========================================================================== */
|
||||
.fc-sidebar-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 1.2rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
color: var(--text-main);
|
||||
transition: all 0.2s;
|
||||
width: calc(100% - 16px);
|
||||
margin: 2px 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.fc-sidebar-tab.active {
|
||||
background: #111827; /* Gris très foncé typique des sidebars */
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.fc-sidebar-tab:hover:not(.active) {
|
||||
background: var(--bg-soft, #f1f5f9);
|
||||
}
|
||||
|
||||
/* Variante Dark Mode native pour l'onglet actif */
|
||||
[data-theme="dark"] .fc-sidebar-tab.active {
|
||||
background: var(--primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,106 @@
|
||||
<?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 ───
|
||||
if ($action === 'get_all') {
|
||||
// A. Options globales du foyer
|
||||
$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' => '["Nounou","Centre"]'];
|
||||
|
||||
// B. Liste des membres de la famille
|
||||
$stmtPeople = $pdo->query("SELECT id, name, role FROM pf_people WHERE is_active = 1 ORDER BY role DESC, name ASC");
|
||||
$people = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// C. Matrice des congés par personne
|
||||
$stmtLeaves = $pdo->query("SELECT person_id, leave_type, anniversary_date FROM pf_person_leave_meta");
|
||||
$leavesRaw = $stmtLeaves->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// On organise les congés par ID de personne pour faciliter le traitement côté JS
|
||||
$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']
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'foyer' => [
|
||||
'zone_scolaire' => $foyer['zone_scolaire'],
|
||||
'care_modes' => json_decode($foyer['care_modes'] ?? '[]', true)
|
||||
],
|
||||
'people' => $people,
|
||||
'leaves' => $leavesMap
|
||||
]
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ─── 2. SAUVEGARDE DU FOYER (ONGLET 1) ───
|
||||
if ($action === 'save_foyer') {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Error("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 CONGÉS D'UN MEMBRE (ONGLET 2) ───
|
||||
if ($action === 'save_member_leaves') {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Error("Méthode non autorisée");
|
||||
|
||||
$personId = (int)($_POST['person_id'] ?? 0);
|
||||
$leavesData = json_decode($_POST['leaves'] ?? '[]', true);
|
||||
|
||||
if ($personId <= 0) throw new Exception("Identifiant de membre invalide.");
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// On nettoie les anciennes configurations de congés de cette personne
|
||||
$stmtDelete = $pdo->prepare("DELETE FROM pf_person_leave_meta WHERE person_id = ?");
|
||||
$stmtDelete->execute([$personId]);
|
||||
|
||||
// On réinsère la nouvelle matrice propre
|
||||
if (!empty($leavesData)) {
|
||||
$stmtInsert = $pdo->prepare("INSERT INTO pf_person_leave_meta (person_id, leave_type, method, allowance, anniversary_date) VALUES (?, ?, ?, ?, ?)");
|
||||
foreach ($leavesData as $leave) {
|
||||
$type = strtoupper(trim($leave['type']));
|
||||
$method = in_array($leave['method'], ['FIXED', 'ACCUMULATED']) ? $leave['method'] : 'FIXED';
|
||||
$allowance = (float)($leave['allowance'] ?? 0);
|
||||
$date = trim($leave['date']);
|
||||
|
||||
if (!empty($type) && !empty($date)) {
|
||||
$stmtInsert->execute([$personId, $type, $method, $allowance, $date]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
throw new Exception("Action inconnue");
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
exit;
|
||||
}
|
||||
Reference in New Issue
Block a user