1 parent
536f479423
commit
f19a7442de
10 files changed
+529
-159
No files matched your search
@@ -88,7 +88,8 @@ CREATE TABLE IF NOT EXISTS pf_leave_snapshots (
|
|||||||
person_id INT NOT NULL,
|
person_id INT NOT NULL,
|
||||||
leave_type VARCHAR(50) NOT NULL,
|
leave_type VARCHAR(50) NOT NULL,
|
||||||
snapshot_date DATE NOT NULL,
|
snapshot_date DATE NOT NULL,
|
||||||
remaining_balance DECIMAL(6,2) DEFAULT 0
|
remaining_balance DECIMAL(6,2) DEFAULT 0.00,
|
||||||
|
carry_over_balance DECIMAL(6,2) DEFAULT 0.00
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_person_leave_meta (
|
CREATE TABLE IF NOT EXISTS pf_person_leave_meta (
|
||||||
@@ -97,9 +98,12 @@ CREATE TABLE IF NOT EXISTS pf_person_leave_meta (
|
|||||||
leave_type VARCHAR(50) COLLATE utf8mb4_unicode_ci NOT NULL,
|
leave_type VARCHAR(50) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||||
method VARCHAR(20) COLLATE utf8mb4_unicode_ci DEFAULT 'FIXED',
|
method VARCHAR(20) COLLATE utf8mb4_unicode_ci DEFAULT 'FIXED',
|
||||||
allowance DECIMAL(5,2) DEFAULT 0.00,
|
allowance DECIMAL(5,2) DEFAULT 0.00,
|
||||||
|
carry_over_max_days DECIMAL(5,2) DEFAULT 0.00,
|
||||||
|
carry_over_deadline_month INT(11) DEFAULT 12,
|
||||||
anniversary_date DATE DEFAULT NULL,
|
anniversary_date DATE DEFAULT NULL,
|
||||||
INDEX (person_id),
|
INDEX (person_id),
|
||||||
INDEX (leave_type)
|
INDEX (leave_type),
|
||||||
|
UNIQUE KEY uq_person_leave (person_id, leave_type)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- ─── Types de Congés (Référentiel) ─────────────────────────────────────────
|
-- ─── Types de Congés (Référentiel) ─────────────────────────────────────────
|
||||||
@@ -109,12 +113,11 @@ CREATE TABLE IF NOT EXISTS pf_leave_types (
|
|||||||
label VARCHAR(100) COLLATE utf8mb4_general_ci NOT NULL,
|
label VARCHAR(100) COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
default_allowance DECIMAL(5,2) DEFAULT 0.00,
|
default_allowance DECIMAL(5,2) DEFAULT 0.00,
|
||||||
reset_month INT(11) DEFAULT 1,
|
reset_month INT(11) DEFAULT 1,
|
||||||
allow_carry_over TINYINT(1) DEFAULT 0,
|
|
||||||
UNIQUE KEY code (code)
|
UNIQUE KEY code (code)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
INSERT IGNORE INTO pf_leave_types (code, label, default_allowance, reset_month, allow_carry_over) VALUES
|
INSERT IGNORE INTO pf_leave_types (code, label, default_allowance, reset_month) VALUES
|
||||||
('CP', 'Congés Payés', 25.00, 6, 0);
|
('CP', 'Congés Payés', 25.00, 6);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_calendar_weeks (
|
CREATE TABLE IF NOT EXISTS pf_calendar_weeks (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
|||||||
+38
-4
@@ -185,17 +185,32 @@ require __DIR__ . '/header.php';
|
|||||||
<div class="pf-form-group">
|
<div class="pf-form-group">
|
||||||
<label class="pf-label"><?= tr('fc_label_remaining_balance') ?></label>
|
<label class="pf-label"><?= tr('fc_label_remaining_balance') ?></label>
|
||||||
<div style="display:flex; align-items:center;">
|
<div style="display:flex; align-items:center;">
|
||||||
<input type="number" step="0.5" id="snapBalance" class="pf-input" placeholder="<?= tr('fc_placeholder_balance') ?>" style="border-radius: 8px 0 0 8px; border-right: none; font-weight: bold; color: var(--pf-primary);" required>
|
<!-- 🔥 STEP 0.01 POUR DEUX DÉCIMALES STRICTES -->
|
||||||
|
<input type="number" step="0.01" id="snapBalance" class="pf-input" placeholder="<?= tr('fc_placeholder_balance') ?>" style="border-radius: 8px 0 0 8px; border-right: none; font-weight: bold; color: var(--pf-primary);" required>
|
||||||
<span style="background: var(--pf-bg-lighter); border: 1px solid var(--pf-border); padding: 0 12px; height: 42px; display: flex; align-items: center; border-radius: 0 8px 8px 0; color: var(--pf-text-muted); font-size: 0.9rem; font-weight: 600;">
|
<span style="background: var(--pf-bg-lighter); border: 1px solid var(--pf-border); padding: 0 12px; height: 42px; display: flex; align-items: center; border-radius: 0 8px 8px 0; color: var(--pf-text-muted); font-size: 0.9rem; font-weight: 600;">
|
||||||
<?= tr('fc_unit_days') ?>
|
<?= tr('fc_unit_days') ?>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<!-- NOUVEAU BLOC : Solde reporté -->
|
||||||
|
<div class="pf-form-group" style="margin-top: 15px;">
|
||||||
|
<label class="pf-label"><?= tr('fc_label_carry_over') ?></label>
|
||||||
|
<div style="display:flex; align-items:center;">
|
||||||
|
<!-- 🔥 STEP 0.01 POUR DEUX DÉCIMALES STRICTES -->
|
||||||
|
<input type="number" step="0.01" id="snapCarryOver" class="pf-input" placeholder="ex: 2.00" value="0" style="border-radius: 8px 0 0 8px; border-right: none; font-weight: bold; color: var(--danger);" required>
|
||||||
|
<span style="background: var(--pf-bg-lighter); border: 1px solid var(--pf-border); padding: 0 12px; height: 42px; display: flex; align-items: center; border-radius: 0 8px 8px 0; color: var(--pf-text-muted); font-size: 0.9rem; font-weight: 600;">
|
||||||
|
<?= tr('fc_unit_days') ?>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-modal-footer">
|
<small class="pf-muted-note"><?= tr('fc_carry_over_note') ?></small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FOOTER INTÉGRÉ DANS LE FORMULAIRE POUR ÉVITER LE BUG DE CIBLAGE -->
|
||||||
|
<div class="pf-modal-footer" style="margin-top: 20px; padding-top: 15px; border-top: 1px solid var(--pf-border); display: flex; justify-content: flex-end; gap: 10px;">
|
||||||
<button type="button" class="pf-btn pf-btn-secondary" onclick="document.getElementById('modalSnapshot').classList.remove('open'); document.body.classList.remove('no-scroll');"><?= tr('btn_cancel') ?></button>
|
<button type="button" class="pf-btn pf-btn-secondary" onclick="document.getElementById('modalSnapshot').classList.remove('open'); document.body.classList.remove('no-scroll');"><?= tr('btn_cancel') ?></button>
|
||||||
<button type="submit" form="formSnapshot" class="pf-btn pf-btn-primary"><?= tr('fc_btn_save_snap') ?></button>
|
<button type="submit" id="btnSubmitSnapshot" class="pf-btn pf-btn-primary"><?= tr('fc_btn_save_snap') ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -289,6 +304,25 @@ require __DIR__ . '/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-top: 5px;">
|
||||||
|
<div style="flex: 1; min-width: 140px;">
|
||||||
|
<label class="pf-label"><?= tr('fc_carry_max') ?></label>
|
||||||
|
<input type="number" step="0.5" id="lt-carry-max" class="pf-input" placeholder="ex: 2" value="0" required>
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; min-width: 140px;">
|
||||||
|
<label class="pf-label"><?= tr('fc_carry_deadline') ?></label>
|
||||||
|
<select id="lt-carry-deadline" class="pf-input">
|
||||||
|
<option value="1"><?= tr('month_01') ?></option>
|
||||||
|
<option value="2"><?= tr('month_02') ?></option>
|
||||||
|
<option value="3"><?= tr('month_03') ?></option>
|
||||||
|
<option value="4"><?= tr('month_04') ?></option>
|
||||||
|
<option value="5"><?= tr('month_05') ?></option>
|
||||||
|
<option value="6"><?= tr('month_06') ?></option>
|
||||||
|
<option value="12"><?= tr('fc_deadline_none') ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 5px;">
|
<div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 5px;">
|
||||||
<button type="button" class="pf-btn btn-secondary" onclick="resetLeaveTypeForm()"><?= tr('btn_cancel') ?></button>
|
<button type="button" class="pf-btn btn-secondary" onclick="resetLeaveTypeForm()"><?= tr('btn_cancel') ?></button>
|
||||||
<button type="button" class="pf-btn pf-btn-primary" onclick="saveLeaveType()"><?= tr('btn_save') ?></button>
|
<button type="button" class="pf-btn pf-btn-primary" onclick="saveLeaveType()"><?= tr('btn_save') ?></button>
|
||||||
|
|||||||
@@ -280,6 +280,21 @@ return [
|
|||||||
|
|
||||||
'btn_save_rights' => 'Guardar',
|
'btn_save_rights' => 'Guardar',
|
||||||
|
|
||||||
|
'fc_label_carry_over' => 'Inclou dies reportats (Caducables) ⚠️',
|
||||||
|
'fc_carry_over_note' => 'Part del saldo global que caducarà a la data límit.',
|
||||||
|
'fc_carry_max' => 'Màx Reportable',
|
||||||
|
'fc_carry_deadline' => 'Mes de Caducitat',
|
||||||
|
'fc_deadline_none' => 'Cap',
|
||||||
|
'fc_allowance_help' => 'Fix: Quota anual. Gradual: Afegit per mes.',
|
||||||
|
'fc_carry_max_short' => 'Report màx',
|
||||||
|
'fc_edit_leave' => '✏️ Modificar:',
|
||||||
|
'fc_not_modifiable' => 'No modificable',
|
||||||
|
'fc_add_leave' => '+ Afegir',
|
||||||
|
'fc_err_code_label' => 'El Codi i l\'Etiqueta són obligatoris.',
|
||||||
|
'fc_catalog_updated' => 'Catàleg actualitzat!',
|
||||||
|
'fc_balance_corrected' => 'Saldo corregit!',
|
||||||
|
'fc_no_leave_configured' => 'Cap permís configurat.',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// MODAL DE CONFIGURACIÓ DEL CALENDARI
|
// MODAL DE CONFIGURACIÓ DEL CALENDARI
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
@@ -281,6 +281,21 @@ return [
|
|||||||
|
|
||||||
'btn_save_rights' => 'Save',
|
'btn_save_rights' => 'Save',
|
||||||
|
|
||||||
|
'fc_label_carry_over' => 'Incl. carried over days (Expiring) ⚠️',
|
||||||
|
'fc_carry_over_note' => 'Portion of the overall balance that will expire on the deadline.',
|
||||||
|
'fc_carry_max' => 'Max Carry Over',
|
||||||
|
'fc_carry_deadline' => 'Expiration Month',
|
||||||
|
'fc_deadline_none' => 'None',
|
||||||
|
'fc_allowance_help' => 'Fixed: Annual quota. Accrued: Added per month.',
|
||||||
|
'fc_carry_max_short' => 'Max carry-over',
|
||||||
|
'fc_edit_leave' => '✏️ Edit:',
|
||||||
|
'fc_not_modifiable' => 'Not modifiable',
|
||||||
|
'fc_add_leave' => '+ Add',
|
||||||
|
'fc_err_code_label' => 'Code and Label are required.',
|
||||||
|
'fc_catalog_updated' => 'Catalog updated!',
|
||||||
|
'fc_balance_corrected' => 'Balance corrected!',
|
||||||
|
'fc_no_leave_configured' => 'No leave configured.',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// CALENDAR SETTINGS MODAL
|
// CALENDAR SETTINGS MODAL
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
@@ -283,6 +283,21 @@ return [
|
|||||||
|
|
||||||
'btn_save_rights' => 'Enregistrer',
|
'btn_save_rights' => 'Enregistrer',
|
||||||
|
|
||||||
|
'fc_label_carry_over' => 'Dont jours reportés (Périssables) ⚠️',
|
||||||
|
'fc_carry_over_note' => 'Partie du solde global qui expirera à la date butoir définie.',
|
||||||
|
'fc_carry_max' => 'Max Reportable',
|
||||||
|
'fc_carry_deadline' => 'Mois Expiration',
|
||||||
|
'fc_deadline_none' => 'Aucune',
|
||||||
|
'fc_allowance_help' => 'Fixe: Quota annuel. Graduel: Ajout par mois.',
|
||||||
|
'fc_carry_max_short' => 'Report max',
|
||||||
|
'fc_edit_leave' => '✏️ Modifier :',
|
||||||
|
'fc_not_modifiable' => 'Non modifiable',
|
||||||
|
'fc_add_leave' => '+ Ajouter',
|
||||||
|
'fc_err_code_label' => 'Le code et le libellé sont obligatoires.',
|
||||||
|
'fc_catalog_updated' => 'Catalogue mis à jour !',
|
||||||
|
'fc_balance_corrected' => 'Solde corrigé !',
|
||||||
|
'fc_no_leave_configured' => 'Aucun congé configuré.',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// MODALE PARAMÈTRES CALENDRIER
|
// MODALE PARAMÈTRES CALENDRIER
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
+33
-30
@@ -1,48 +1,51 @@
|
|||||||
<?php
|
<?php
|
||||||
|
// migrate_leaves.php
|
||||||
require_once __DIR__ . '/includes/meta_db.php';
|
require_once __DIR__ . '/includes/meta_db.php';
|
||||||
|
|
||||||
// Récupération des identifiants de BDD depuis l'environnement (fallback par défaut)
|
// Détection stricte de l'environnement (Docker vs XAMPP)
|
||||||
$db_host = getenv('DB_HOST') ?: 'househub-db';
|
$envHost = getenv('DB_HOST');
|
||||||
$db_user = getenv('DB_USER') ?: 'househub';
|
if ($envHost === 'househub-db') {
|
||||||
$db_pass = getenv('DB_PASS') ?: 'changeme';
|
$host = 'househub-db';
|
||||||
|
$user = getenv('DB_USER') ?: 'househub';
|
||||||
echo "<h1>🛠️ HouseHub - Correction AUTO_INCREMENT Voyages</h1>";
|
$pass = getenv('DB_PASS') ?: 'changeme';
|
||||||
|
} else {
|
||||||
|
$host = '127.0.0.1';
|
||||||
|
$user = 'root';
|
||||||
|
$pass = '';
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// On récupère toutes les familles actives
|
$stmt = $meta_pdo->query("SELECT db_name FROM families");
|
||||||
$stmt = $meta_pdo->query("SELECT db_name, name FROM families");
|
|
||||||
$families = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$families = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
foreach ($families as $f) {
|
foreach ($families as $fam) {
|
||||||
$dbName = $f['db_name'];
|
$db = $fam['db_name'];
|
||||||
echo "<h3>Famille : {$f['name']} ($dbName)</h3><ul>";
|
echo "Migration sur la base : $db...<br>";
|
||||||
|
|
||||||
try {
|
$pdo = new PDO("mysql:host=$host;dbname=$db;charset=utf8mb4", $user, $pass, [
|
||||||
$pdo = new PDO("mysql:host=$db_host;dbname=$dbName;charset=utf8mb4", $db_user, $db_pass, [
|
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 1. On vérifie si la clé primaire existe déjà pour éviter de faire planter le script
|
// 1. pf_person_leave_meta : Règles de report individuelles
|
||||||
$hasPrimaryKey = $pdo->query("SHOW KEYS FROM pf_holidays WHERE Key_name = 'PRIMARY'")->fetch();
|
$pdo->exec("ALTER TABLE pf_person_leave_meta ADD COLUMN IF NOT EXISTS carry_over_max_days DECIMAL(5,2) DEFAULT 0.00 AFTER allowance");
|
||||||
|
$pdo->exec("ALTER TABLE pf_person_leave_meta ADD COLUMN IF NOT EXISTS carry_over_deadline_month INT(11) DEFAULT 12 AFTER carry_over_max_days");
|
||||||
|
|
||||||
if (!$hasPrimaryKey) {
|
// 2. pf_leave_snapshots : Ajout du solde de report périssable
|
||||||
$pdo->exec("ALTER TABLE pf_holidays ADD PRIMARY KEY (id)");
|
$pdo->exec("ALTER TABLE pf_leave_snapshots ADD COLUMN IF NOT EXISTS carry_over_balance DECIMAL(6,2) DEFAULT 0.00 AFTER remaining_balance");
|
||||||
echo "<li>✅ Clé primaire restaurée sur la colonne `id`.</li>";
|
|
||||||
|
// 3. pf_leave_types : Nettoyage de l'ancien booléen global s'il existe
|
||||||
|
try {
|
||||||
|
$pdo->exec("ALTER TABLE pf_leave_types DROP COLUMN allow_carry_over");
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// La colonne n'existait pas ou est déjà supprimée
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. On restaure la propriété AUTO_INCREMENT
|
echo "✅ Migration réussie pour $db.<br><br>";
|
||||||
$pdo->exec("ALTER TABLE pf_holidays MODIFY id INT(11) NOT NULL AUTO_INCREMENT");
|
|
||||||
echo "<li>✅ Propriété AUTO_INCREMENT restaurée.</li>";
|
|
||||||
|
|
||||||
} catch (\PDOException $e) {
|
|
||||||
// On attrape l'erreur si la modif a déjà été faite
|
|
||||||
echo "<li>ℹ️ Action ignorée ou table déjà à jour : " . $e->getMessage() . "</li>";
|
|
||||||
}
|
}
|
||||||
echo "</ul>";
|
|
||||||
}
|
|
||||||
echo "<h2>🚀 Correction terminée ! Tu peux réessayer de créer un voyage.</h2>";
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
echo "🎉 <strong>Toutes les bases ont été mises à jour !</strong> Vous pouvez supprimer ce fichier.";
|
||||||
die("Erreur fatale de connexion à la Meta DB : " . $e->getMessage());
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die("❌ Erreur fatale : " . $e->getMessage());
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
@@ -198,20 +198,21 @@ function renderLeaveCatalog(types) {
|
|||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
|
|
||||||
if (types.length === 0) {
|
if (types.length === 0) {
|
||||||
container.innerHTML = `<p class="pf-muted-note">Aucun congé configuré.</p>`;
|
container.innerHTML = `<p class="pf-muted-note">${tr("fc_no_leave_configured") || "Aucun congé configuré."}</p>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
types.forEach((lt) => {
|
types.forEach((lt) => {
|
||||||
|
// Fini l'injection des variables de report ici, elles n'existent plus au niveau global !
|
||||||
container.innerHTML += `
|
container.innerHTML += `
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; background: var(--bg-page); padding: 8px 12px; border-radius: 6px; border: 1px solid var(--border-light); margin-bottom: 6px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; background: var(--bg-page); padding: 8px 12px; border-radius: 8px; border: 1px solid var(--border-light); margin-bottom: 8px;">
|
||||||
<div>
|
<div>
|
||||||
<span style="font-weight: bold; color: var(--text-main);">${lt.label}</span>
|
<span style="font-weight: 700; color: var(--text-main); font-size: 1.05rem;">${lt.label}</span>
|
||||||
<span style="background: var(--bg-subtle); border: 1px solid var(--border-main); border-radius: 4px; padding: 2px 6px; font-size: 0.8rem; margin-left: 8px; color: var(--text-muted);">${lt.code}</span>
|
<span style="background: var(--bg-subtle); border: 1px solid var(--border-strong); border-radius: 6px; padding: 2px 8px; font-size: 0.75rem; margin-left: 8px; color: var(--text-muted); font-weight: bold;">${lt.code}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; gap: 4px;">
|
<div style="display: flex; gap: 8px;">
|
||||||
<button type="button" class="pf-btn btn-secondary" style="padding: 4px 8px;" onclick="editLeaveType('${lt.code}', '${lt.label.replace(/'/g, "\\'")}')">✏️</button>
|
<button type="button" class="btn-icon-action edit" onclick="editLeaveType('${lt.code}', '${lt.label.replace(/'/g, "\\'")}')" title="${tr("fc_edit_leave") || "Modifier"}">✏️</button>
|
||||||
<button type="button" class="pf-btn btn-secondary" style="padding: 4px 8px; color: var(--danger);" onclick="deleteLeaveType('${lt.code}', '${lt.label.replace(/'/g, "\\'")}')">🗑️</button>
|
<button type="button" class="btn-icon-action delete" onclick="deleteLeaveType('${lt.code}', '${lt.label.replace(/'/g, "\\'")}')" title="${tr("btn_delete") || "Supprimer"}">🗑️</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -220,7 +221,7 @@ function renderLeaveCatalog(types) {
|
|||||||
|
|
||||||
function editLeaveType(code, label) {
|
function editLeaveType(code, label) {
|
||||||
document.getElementById("leaveTypeFormTitle").innerText =
|
document.getElementById("leaveTypeFormTitle").innerText =
|
||||||
"✏️ Modifier : " + label;
|
(tr("fc_edit_leave") || "✏️ Modifier :") + " " + label;
|
||||||
document.getElementById("lt-mode").value = "edit";
|
document.getElementById("lt-mode").value = "edit";
|
||||||
|
|
||||||
const codeInput = document.getElementById("lt-code");
|
const codeInput = document.getElementById("lt-code");
|
||||||
@@ -230,12 +231,14 @@ function editLeaveType(code, label) {
|
|||||||
codeInput.style.opacity = "0.6";
|
codeInput.style.opacity = "0.6";
|
||||||
codeInput.style.cursor = "not-allowed";
|
codeInput.style.cursor = "not-allowed";
|
||||||
|
|
||||||
document.getElementById("lt-code-note").innerText = "Non modifiable";
|
document.getElementById("lt-code-note").innerText =
|
||||||
|
tr("fc_not_modifiable") || "Non modifiable";
|
||||||
document.getElementById("lt-label").value = label;
|
document.getElementById("lt-label").value = label;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetLeaveTypeForm() {
|
function resetLeaveTypeForm() {
|
||||||
document.getElementById("leaveTypeFormTitle").innerText = "+ Ajouter";
|
document.getElementById("leaveTypeFormTitle").innerText =
|
||||||
|
tr("fc_add_leave") || "+ Ajouter";
|
||||||
document.getElementById("lt-mode").value = "add";
|
document.getElementById("lt-mode").value = "add";
|
||||||
|
|
||||||
const codeInput = document.getElementById("lt-code");
|
const codeInput = document.getElementById("lt-code");
|
||||||
@@ -245,7 +248,8 @@ function resetLeaveTypeForm() {
|
|||||||
codeInput.style.opacity = "1";
|
codeInput.style.opacity = "1";
|
||||||
codeInput.style.cursor = "text";
|
codeInput.style.cursor = "text";
|
||||||
|
|
||||||
document.getElementById("lt-code-note").innerText = "Irréversible";
|
document.getElementById("lt-code-note").innerText =
|
||||||
|
tr("fc_code_irreversible") || "Irréversible";
|
||||||
document.getElementById("lt-label").value = "";
|
document.getElementById("lt-label").value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,17 +287,21 @@ async function deleteLeaveType(code, label) {
|
|||||||
async function saveLeaveType() {
|
async function saveLeaveType() {
|
||||||
const code = document.getElementById("lt-code").value.toUpperCase();
|
const code = document.getElementById("lt-code").value.toUpperCase();
|
||||||
const label = document.getElementById("lt-label").value;
|
const label = document.getElementById("lt-label").value;
|
||||||
|
const carryMax = document.getElementById("lt-carry-max").value;
|
||||||
|
const carryDeadline = document.getElementById("lt-carry-deadline").value;
|
||||||
|
|
||||||
if (!code || !label)
|
if (!code || !label)
|
||||||
return window.showToast
|
return window.showToast
|
||||||
? showToast("Code et Label requis", "error")
|
? showToast(tr("fc_err_code_label"), "error")
|
||||||
: alert("Code et Label requis");
|
: alert(tr("fc_err_code_label"));
|
||||||
|
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append("action", "save_leave_type");
|
fd.append("action", "save_leave_type");
|
||||||
fd.append("mode", document.getElementById("lt-mode").value);
|
fd.append("mode", document.getElementById("lt-mode").value);
|
||||||
fd.append("code", code);
|
fd.append("code", code);
|
||||||
fd.append("label", label);
|
fd.append("label", label);
|
||||||
|
fd.append("carry_over_max_days", carryMax);
|
||||||
|
fd.append("carry_over_deadline_month", carryDeadline);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await pachaFetch(
|
const res = await pachaFetch(
|
||||||
@@ -302,11 +310,11 @@ async function saveLeaveType() {
|
|||||||
);
|
);
|
||||||
if (!res.success) throw new Error(res.error);
|
if (!res.success) throw new Error(res.error);
|
||||||
|
|
||||||
if (window.showToast) showToast("Catalogue mis à jour !", "success");
|
if (window.showToast) showToast(tr("fc_catalog_updated"), "success");
|
||||||
resetLeaveTypeForm();
|
resetLeaveTypeForm();
|
||||||
loadLeaveCatalog();
|
loadLeaveCatalog();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("Erreur d'enregistrement : " + err.message);
|
alert(tr("error_occured") + " " + err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,84 +381,194 @@ async function loadMemberConfigView() {
|
|||||||
|
|
||||||
function renderMemberLeavesView(personId, memberLeaves) {
|
function renderMemberLeavesView(personId, memberLeaves) {
|
||||||
const zone = document.getElementById("memberConfigZone");
|
const zone = document.getElementById("memberConfigZone");
|
||||||
let html = `<h5 style="margin: 0 0 10px 0; color: var(--text-main);">Congés attribués</h5>`;
|
let html = `<h5 style="margin: 0 0 15px 0; color: var(--text-main); font-size: 1.05rem;">Congés attribués</h5>`;
|
||||||
|
|
||||||
if (memberLeaves.length === 0) {
|
if (memberLeaves.length === 0) {
|
||||||
html += `<p class="pf-muted-note" style="font-size: 0.85rem;">Aucun congé attribué à ce membre.</p>`;
|
html += `<p class="pf-muted-note" style="font-size: 0.9rem; font-style: italic;">Aucun congé attribué à ce membre.</p>`;
|
||||||
} else {
|
} else {
|
||||||
html += `<div style="display: flex; flex-direction: column; gap: 8px; margin-bottom: 15px;">`;
|
html += `<div style="display: flex; flex-direction: column; gap: 12px; margin-bottom: 25px;">`;
|
||||||
memberLeaves.forEach((ml) => {
|
memberLeaves.forEach((ml) => {
|
||||||
const moisRenouv = ml.anniversary_date
|
const moisRenouv = ml.anniversary_date
|
||||||
? parseInt(ml.anniversary_date.split("-")[1])
|
? parseInt(ml.anniversary_date.split("-")[1])
|
||||||
: 1;
|
: 1;
|
||||||
const methodeLabel = ml.method === "ACCUMULATED" ? "Graduel" : "Fixe";
|
const isGraduel = ml.method === "ACCUMULATED";
|
||||||
|
const methodeLabel = isGraduel ? "Graduel" : "Fixe";
|
||||||
|
|
||||||
|
// Construction des petits Badges
|
||||||
|
let badgesHtml = `
|
||||||
|
<span style="background: var(--bg-page); border: 1px solid var(--border-light); padding: 2px 8px; border-radius: 12px; font-size: 0.75rem; color: var(--text-muted); font-weight: 600;">
|
||||||
|
⚖️ ${ml.allowance} j ${isGraduel ? "/ mois" : "/ an"}
|
||||||
|
</span>
|
||||||
|
<span style="background: var(--bg-page); border: 1px solid var(--border-light); padding: 2px 8px; border-radius: 12px; font-size: 0.75rem; color: var(--text-muted); font-weight: 600;">
|
||||||
|
🔄 Mois ${moisRenouv}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (ml.carry_over_max_days > 0) {
|
||||||
|
badgesHtml += `
|
||||||
|
<span style="background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.3); padding: 2px 8px; border-radius: 12px; font-size: 0.75rem; color: #d97706; font-weight: 600;">
|
||||||
|
⚠️ Report Max: ${ml.carry_over_max_days}j (Exp. M${ml.carry_over_deadline_month})
|
||||||
|
</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; background: var(--bg-subtle); padding: 8px; border-radius: 6px; border: 1px solid var(--border-light);">
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; background: var(--bg-subtle); padding: 12px 15px; border-radius: 10px; border: 1px solid var(--border-light);">
|
||||||
<div>
|
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||||||
<strong style="color: var(--text-main);">${ml.leave_type}</strong>
|
<strong style="color: var(--text-main); font-size: 1rem; line-height: 1;">${ml.leave_type} <span style="font-weight: normal; font-size: 0.8rem; color: var(--text-muted);">— ${methodeLabel}</span></strong>
|
||||||
<span style="font-size: 0.8rem; color: var(--text-muted); margin-left: 5px;">
|
<div style="display: flex; gap: 6px; flex-wrap: wrap;">
|
||||||
(Quota: <b>${ml.allowance}j</b> - Renouv: <b>Mois ${moisRenouv}</b> - Acquis: <b>${methodeLabel}</b>)
|
${badgesHtml}
|
||||||
</span>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 6px; margin-left: 15px;">
|
||||||
|
<button class="pf-btn btn-secondary" style="padding: 6px; height: 32px; width: 32px; display: flex; align-items: center; justify-content: center;" onclick="editMemberLeave(${ml.id}, '${ml.leave_type}', ${ml.allowance}, ${moisRenouv}, '${ml.method}', ${ml.carry_over_max_days}, ${ml.carry_over_deadline_month})" title="Modifier">✏️</button>
|
||||||
|
<button class="pf-btn btn-secondary" style="padding: 6px; height: 32px; width: 32px; display: flex; align-items: center; justify-content: center; color: var(--danger);" onclick="deleteMemberLeave(${ml.id}, ${personId})" title="Supprimer">🗑️</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="pf-btn btn-secondary" style="padding: 2px 6px; color: var(--danger);" onclick="deleteMemberLeave(${ml.id}, ${personId})">🗑️</button>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- LE FORMULAIRE RESTRUCTURÉ EN GRILLE ---
|
||||||
html += `
|
html += `
|
||||||
<hr style="border: 0; border-top: 1px solid var(--border-light); margin: 15px 0;">
|
<hr style="border: 0; border-top: 1px solid var(--border-light); margin: 20px 0;">
|
||||||
<h5 style="margin: 0 0 10px 0;">+ Attribuer un congé</h5>
|
<h5 id="formMemberLeaveTitle" style="margin: 0 0 15px 0; font-size: 1.05rem; color: var(--text-main);">+ Attribuer un congé</h5>
|
||||||
<div style="display: flex; gap: 8px; align-items: flex-end; flex-wrap: wrap;">
|
<input type="hidden" id="new-member-leave-id" value="">
|
||||||
<div style="flex: 1; min-width: 120px;">
|
|
||||||
<label class="pf-label" style="font-size: 0.8rem;">Type de congé</label>
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 15px;">
|
||||||
<select id="new-member-leave-type" class="pf-input" style="width: 100%;">
|
<div>
|
||||||
|
<label class="pf-label" style="font-size: 0.85rem;">Type de congé</label>
|
||||||
|
<select id="new-member-leave-type" class="pf-input">
|
||||||
<option value="" disabled selected>-- Sélectionner --</option>
|
<option value="" disabled selected>-- Sélectionner --</option>
|
||||||
${globalLeaveCatalog.map((lt) => `<option value="${lt.code}">${lt.label}</option>`).join("")}
|
${globalLeaveCatalog.map((lt) => `<option value="${lt.code}">${lt.label}</option>`).join("")}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex: 0 0 70px;">
|
|
||||||
<label class="pf-label" style="font-size: 0.8rem;">Quota</label>
|
<div>
|
||||||
<input type="number" step="0.5" id="new-member-leave-allowance" class="pf-input" value="0" style="width: 100%;">
|
<label class="pf-label" style="font-size: 0.85rem;">Acquisition</label>
|
||||||
</div>
|
<select id="new-member-leave-method" class="pf-input">
|
||||||
<div style="flex: 0 0 100px;">
|
<option value="FIXED">Fixe (Annuelle)</option>
|
||||||
<label class="pf-label" style="font-size: 0.8rem;">Renouvellement</label>
|
<option value="ACCUMULATED">Graduelle (Mensuelle)</option>
|
||||||
<select id="new-member-leave-reset" class="pf-input" style="width: 100%;">
|
|
||||||
<option value="1">${tr("month_jan") || "Janvier"}</option>
|
|
||||||
<option value="2">${tr("month_feb") || "Février"}</option>
|
|
||||||
<option value="3">${tr("month_mar") || "Mars"}</option>
|
|
||||||
<option value="4">${tr("month_apr") || "Avril"}</option>
|
|
||||||
<option value="5">${tr("month_may") || "Mai"}</option>
|
|
||||||
<option value="6">${tr("month_jun") || "Juin"}</option>
|
|
||||||
<option value="7">${tr("month_jul") || "Juillet"}</option>
|
|
||||||
<option value="8">${tr("month_aug") || "Août"}</option>
|
|
||||||
<option value="9">${tr("month_sep") || "Septembre"}</option>
|
|
||||||
<option value="10">${tr("month_oct") || "Octobre"}</option>
|
|
||||||
<option value="11">${tr("month_nov") || "Novembre"}</option>
|
|
||||||
<option value="12">${tr("month_dec") || "Décembre"}</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex: 0 0 90px;">
|
|
||||||
<label class="pf-label" style="font-size: 0.8rem;">Acquisition</label>
|
<div title="${tr("fc_allowance_help") || "Fixe: Quota annuel. Graduel: Ajout par mois."}">
|
||||||
<select id="new-member-leave-method" class="pf-input" style="width: 100%;">
|
<label class="pf-label" style="font-size: 0.85rem;">Quota</label>
|
||||||
<option value="FIXED">Fixe</option>
|
<input type="number" step="0.01" id="new-member-leave-allowance" class="pf-input" value="0">
|
||||||
<option value="ACCUMULATED">Graduel</option>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="pf-label" style="font-size: 0.85rem;">Renouvellement</label>
|
||||||
|
<select id="new-member-leave-reset" class="pf-input">
|
||||||
|
<option value="1">${tr("month_01") || "Janvier"}</option>
|
||||||
|
<option value="2">${tr("month_02") || "Février"}</option>
|
||||||
|
<option value="3">${tr("month_03") || "Mars"}</option>
|
||||||
|
<option value="4">${tr("month_04") || "Avril"}</option>
|
||||||
|
<option value="5">${tr("month_05") || "Mai"}</option>
|
||||||
|
<option value="6">${tr("month_06") || "Juin"}</option>
|
||||||
|
<option value="7">${tr("month_07") || "Juillet"}</option>
|
||||||
|
<option value="8">${tr("month_08") || "Août"}</option>
|
||||||
|
<option value="9">${tr("month_09") || "Septembre"}</option>
|
||||||
|
<option value="10">${tr("month_10") || "Octobre"}</option>
|
||||||
|
<option value="11">${tr("month_11") || "Novembre"}</option>
|
||||||
|
<option value="12">${tr("month_12") || "Décembre"}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button class="pf-btn pf-btn-primary" onclick="addMemberLeave(${personId})">Ajouter</button>
|
</div>
|
||||||
|
|
||||||
|
<!-- BLOC AVANCÉ : RÈGLES DE REPORT -->
|
||||||
|
<div style="margin-top: 15px; padding: 12px; background: var(--bg-page); border-left: 3px solid var(--border-strong); border-radius: 4px 8px 8px 4px;">
|
||||||
|
<p style="margin: 0 0 10px 0; font-size: 0.85rem; font-weight: 600; color: var(--text-muted);">⚙️ Règles de report (Optionnel)</p>
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 15px;">
|
||||||
|
<div>
|
||||||
|
<label class="pf-label" style="font-size: 0.8rem;">${tr("fc_carry_max") || "Max Reportable"}</label>
|
||||||
|
<input type="number" step="0.5" id="new-member-leave-carry-max" class="pf-input" value="0" placeholder="ex: 2">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="pf-label" style="font-size: 0.8rem;">${tr("fc_carry_deadline") || "Mois Expiration"}</label>
|
||||||
|
<select id="new-member-leave-carry-deadline" class="pf-input">
|
||||||
|
<option value="1">${tr("month_01") || "Janvier"}</option>
|
||||||
|
<option value="2">${tr("month_02") || "Février"}</option>
|
||||||
|
<option value="3">${tr("month_03") || "Mars"}</option>
|
||||||
|
<option value="4">${tr("month_04") || "Avril"}</option>
|
||||||
|
<option value="12" selected>${tr("fc_deadline_none") || "Aucune"}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
|
||||||
|
<button class="pf-btn btn-secondary" style="display: none;" id="btn-cancel-member-leave" onclick="resetMemberLeaveForm()">${tr("btn_cancel") || "Annuler"}</button>
|
||||||
|
<button class="pf-btn pf-btn-primary" id="btn-submit-member-leave" onclick="addMemberLeave(${personId})">Ajouter / Enregistrer</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
zone.innerHTML = html;
|
zone.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === NOUVELLES FONCTIONS : ÉDITION ET RESET ===
|
||||||
|
window.editMemberLeave = function (
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
allowance,
|
||||||
|
resetMonth,
|
||||||
|
method,
|
||||||
|
carryMax,
|
||||||
|
carryDeadline,
|
||||||
|
) {
|
||||||
|
document.getElementById("formMemberLeaveTitle").innerText =
|
||||||
|
"✏️ Modifier le congé";
|
||||||
|
document.getElementById("new-member-leave-id").value = id;
|
||||||
|
|
||||||
|
const typeSelect = document.getElementById("new-member-leave-type");
|
||||||
|
typeSelect.value = type;
|
||||||
|
typeSelect.disabled = true; // On bloque le type car il définit la clé unique
|
||||||
|
typeSelect.style.backgroundColor = "var(--bg-subtle)";
|
||||||
|
|
||||||
|
document.getElementById("new-member-leave-allowance").value = allowance;
|
||||||
|
document.getElementById("new-member-leave-reset").value = resetMonth;
|
||||||
|
document.getElementById("new-member-leave-method").value = method;
|
||||||
|
document.getElementById("new-member-leave-carry-max").value = carryMax || 0;
|
||||||
|
document.getElementById("new-member-leave-carry-deadline").value =
|
||||||
|
carryDeadline || 12;
|
||||||
|
|
||||||
|
document.getElementById("btn-submit-member-leave").innerText =
|
||||||
|
tr("btn_save") || "Enregistrer";
|
||||||
|
document.getElementById("btn-cancel-member-leave").style.display =
|
||||||
|
"inline-flex";
|
||||||
|
};
|
||||||
|
|
||||||
|
window.resetMemberLeaveForm = function () {
|
||||||
|
document.getElementById("formMemberLeaveTitle").innerText =
|
||||||
|
"+ Attribuer un congé";
|
||||||
|
document.getElementById("new-member-leave-id").value = "";
|
||||||
|
|
||||||
|
const typeSelect = document.getElementById("new-member-leave-type");
|
||||||
|
typeSelect.value = "";
|
||||||
|
typeSelect.disabled = false;
|
||||||
|
typeSelect.style.backgroundColor = "";
|
||||||
|
|
||||||
|
document.getElementById("new-member-leave-allowance").value = "0";
|
||||||
|
document.getElementById("new-member-leave-reset").value = "1";
|
||||||
|
document.getElementById("new-member-leave-method").value = "FIXED";
|
||||||
|
document.getElementById("new-member-leave-carry-max").value = "0";
|
||||||
|
document.getElementById("new-member-leave-carry-deadline").value = "12";
|
||||||
|
|
||||||
|
document.getElementById("btn-submit-member-leave").innerText = "Ajouter";
|
||||||
|
document.getElementById("btn-cancel-member-leave").style.display = "none";
|
||||||
|
};
|
||||||
|
|
||||||
async function addMemberLeave(personId) {
|
async function addMemberLeave(personId) {
|
||||||
const leaveCode = document.getElementById("new-member-leave-type").value;
|
const id = document.getElementById("new-member-leave-id").value;
|
||||||
|
const typeSelect = document.getElementById("new-member-leave-type");
|
||||||
|
const leaveCode = typeSelect.value;
|
||||||
const allowance = document.getElementById("new-member-leave-allowance").value;
|
const allowance = document.getElementById("new-member-leave-allowance").value;
|
||||||
const resetMonth = document.getElementById("new-member-leave-reset").value;
|
const resetMonth = document.getElementById("new-member-leave-reset").value;
|
||||||
const method = document.getElementById("new-member-leave-method").value;
|
const method = document.getElementById("new-member-leave-method").value;
|
||||||
|
const carryMax = document.getElementById("new-member-leave-carry-max").value;
|
||||||
|
const carryDeadline = document.getElementById(
|
||||||
|
"new-member-leave-carry-deadline",
|
||||||
|
).value;
|
||||||
|
|
||||||
if (!leaveCode) {
|
if (!leaveCode) {
|
||||||
return window.showToast
|
return window.showToast
|
||||||
@@ -460,11 +578,14 @@ async function addMemberLeave(personId) {
|
|||||||
|
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append("action", "add_person_leave");
|
fd.append("action", "add_person_leave");
|
||||||
|
fd.append("meta_id", id);
|
||||||
fd.append("person_id", personId);
|
fd.append("person_id", personId);
|
||||||
fd.append("leave_type", leaveCode);
|
fd.append("leave_type", leaveCode);
|
||||||
fd.append("allowance", allowance);
|
fd.append("allowance", allowance);
|
||||||
fd.append("reset_month", resetMonth);
|
fd.append("reset_month", resetMonth);
|
||||||
fd.append("method", method);
|
fd.append("method", method);
|
||||||
|
fd.append("carry_over_max_days", carryMax);
|
||||||
|
fd.append("carry_over_deadline_month", carryDeadline);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await pachaFetch(
|
const res = await pachaFetch(
|
||||||
@@ -473,11 +594,46 @@ async function addMemberLeave(personId) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!res.success) throw new Error(res.error);
|
if (!res.success) throw new Error(res.error);
|
||||||
if (window.showToast) showToast("Congé attribué avec succès !", "success");
|
if (window.showToast) showToast("Opération réussie !", "success");
|
||||||
loadMemberConfigView();
|
loadMemberConfigView();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (window.showToast) showToast("Erreur : " + err.message, "error");
|
||||||
|
else alert("Erreur: " + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteMemberLeave(id, personId) {
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
tr("confirm_delete") ||
|
||||||
|
"Voulez-vous vraiment retirer ce congé pour ce membre ?",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("action", "delete_person_leave");
|
||||||
|
fd.append("id", id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await pachaFetch(
|
||||||
|
"/modules/family-calendar/includes/api/calendar-settings.php",
|
||||||
|
{ method: "POST", body: fd },
|
||||||
|
);
|
||||||
|
if (!res.success) throw new Error(res.error);
|
||||||
|
|
||||||
if (window.showToast)
|
if (window.showToast)
|
||||||
showToast("Erreur lors de l'attribution : " + err.message, "error");
|
showToast(
|
||||||
|
tr("fc_settings_updated") || "Congé retiré avec succès !",
|
||||||
|
"success",
|
||||||
|
);
|
||||||
|
loadMemberConfigView(); // Recharge la vue des attributions du membre
|
||||||
|
} catch (err) {
|
||||||
|
if (window.showToast)
|
||||||
|
showToast(
|
||||||
|
(tr("error_occured") || "Erreur") + " : " + err.message,
|
||||||
|
"error",
|
||||||
|
);
|
||||||
else alert("Erreur: " + err.message);
|
else alert("Erreur: " + err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -916,6 +1072,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const balances = {};
|
const balances = {};
|
||||||
this.parents.forEach((p) => (balances[String(p.id)] = {}));
|
this.parents.forEach((p) => (balances[String(p.id)] = {}));
|
||||||
|
|
||||||
|
// 1. Préparation d'une chronologie glissante (60 mois)
|
||||||
const ymList = [];
|
const ymList = [];
|
||||||
const tempDate = new Date();
|
const tempDate = new Date();
|
||||||
tempDate.setFullYear(tempDate.getFullYear() - 2);
|
tempDate.setFullYear(tempDate.getFullYear() - 2);
|
||||||
@@ -926,6 +1083,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
tempDate.setMonth(tempDate.getMonth() + 1);
|
tempDate.setMonth(tempDate.getMonth() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Indexation des jours consommés par mois
|
||||||
const usageByMonth = {};
|
const usageByMonth = {};
|
||||||
const allPlacedLeaves = [...(this.leaves || []), ...(this.events || [])];
|
const allPlacedLeaves = [...(this.leaves || []), ...(this.events || [])];
|
||||||
|
|
||||||
@@ -947,6 +1105,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
usageByMonth[pid][type][ym] = (usageByMonth[pid][type][ym] || 0) + dur;
|
usageByMonth[pid][type][ym] = (usageByMonth[pid][type][ym] || 0) + dur;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 3. Parcours Chronologique
|
||||||
this.parents.forEach((parent) => {
|
this.parents.forEach((parent) => {
|
||||||
const pid = String(parent.id);
|
const pid = String(parent.id);
|
||||||
const matrix =
|
const matrix =
|
||||||
@@ -958,49 +1117,74 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
.toUpperCase();
|
.toUpperCase();
|
||||||
if (!balances[pid][type]) balances[pid][type] = {};
|
if (!balances[pid][type]) balances[pid][type] = {};
|
||||||
|
|
||||||
let monthRenouvellement = 1;
|
// Paramètres du congé
|
||||||
const dateVal = conf.anniversary_date || conf.date;
|
const resetMonth = parseInt(
|
||||||
if (dateVal && dateVal.includes("-")) {
|
conf.date ? conf.date.split("-")[1] : 1,
|
||||||
monthRenouvellement = parseInt(dateVal.split("-")[1], 10) || 1;
|
10,
|
||||||
}
|
);
|
||||||
|
const maxCarry = parseFloat(conf.carry_over_max_days || 0);
|
||||||
|
const deadlineMonth = parseInt(
|
||||||
|
conf.carry_over_deadline_month || 12,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
const method = conf.method || "FIXED";
|
||||||
|
const allowance = parseFloat(conf.allowance || 0);
|
||||||
|
|
||||||
let initialBalance = parseFloat(conf.allowance);
|
let mainBalance = 0;
|
||||||
if (isNaN(initialBalance)) initialBalance = 0;
|
let carryBalance = 0;
|
||||||
|
|
||||||
|
// Filtrer les snapshots de cette personne et de ce congé
|
||||||
|
const snapshots = (this.leaveSnapshots || []).filter(
|
||||||
|
(s) => String(s.person_id) === pid && s.leave_type === type,
|
||||||
|
);
|
||||||
|
|
||||||
ymList.forEach((ym) => {
|
ymList.forEach((ym) => {
|
||||||
const [currYearStr, currMonthStr] = ym.split("-");
|
const currM = parseInt(ym.split("-")[1], 10);
|
||||||
const currYear = parseInt(currYearStr, 10);
|
const snap = snapshots.find((s) => s.snapshot_date.startsWith(ym));
|
||||||
const currMonth = parseInt(currMonthStr, 10);
|
|
||||||
|
|
||||||
const isPastAnniversary = currMonth >= monthRenouvellement;
|
if (snap) {
|
||||||
const refYear = isPastAnniversary ? currYear : currYear - 1;
|
// A. LE MUR DE BÉTON : Le Snapshot écrase tout le passé
|
||||||
const cycleStartStr = `${refYear}-${String(monthRenouvellement).padStart(2, "0")}`;
|
mainBalance = parseFloat(snap.remaining_balance || 0);
|
||||||
|
carryBalance = parseFloat(snap.carry_over_balance || 0);
|
||||||
let acquiredBalance = initialBalance;
|
} else {
|
||||||
if (conf.method === "ACCUMULATED") {
|
// B. LA GUILLOTINE : Effacement du report au mois M+1 de la deadline
|
||||||
let monthsPassed =
|
let wipeMonth = (deadlineMonth % 12) + 1;
|
||||||
(currYear - refYear) * 12 +
|
if (currM === wipeMonth && maxCarry > 0) {
|
||||||
(currMonth - monthRenouvellement) +
|
carryBalance = 0;
|
||||||
1;
|
|
||||||
acquiredBalance = Math.min(
|
|
||||||
initialBalance,
|
|
||||||
(initialBalance / 12) * monthsPassed,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let usedBeforeCurrentMonth = 0;
|
// C. RENOUVELLEMENT ANNUEL (Bascule vers le Report)
|
||||||
Object.keys(usageByMonth[pid]?.[type] || {}).forEach((usedYm) => {
|
if (currM === resetMonth) {
|
||||||
if (usedYm >= cycleStartStr && usedYm < ym) {
|
carryBalance = Math.min(mainBalance, maxCarry);
|
||||||
usedBeforeCurrentMonth += usageByMonth[pid][type][usedYm];
|
mainBalance = 0;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
|
// D. ACQUISITION
|
||||||
|
if (method === "FIXED" && currM === resetMonth) {
|
||||||
|
mainBalance += allowance;
|
||||||
|
} else if (method === "ACCUMULATED") {
|
||||||
|
// Acquisition graduelle stricte (ex: +0.64/mois)
|
||||||
|
mainBalance += allowance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// E. CONSOMMATION DU MOIS
|
||||||
|
const totalAvailableAtStart = mainBalance + carryBalance;
|
||||||
|
const carryAtStart = carryBalance;
|
||||||
|
const usedInMonth = usageByMonth[pid]?.[type]?.[ym] || 0;
|
||||||
|
|
||||||
|
// Priorité absolue : On tape d'abord dans les jours périssables !
|
||||||
|
let usedFromCarry = Math.min(usedInMonth, carryBalance);
|
||||||
|
carryBalance -= usedFromCarry;
|
||||||
|
|
||||||
|
let usedFromMain = usedInMonth - usedFromCarry;
|
||||||
|
mainBalance -= usedFromMain;
|
||||||
|
|
||||||
|
// F. SAUVEGARDE DE L'ÉTAT DU MOIS
|
||||||
balances[pid][type][ym] = {
|
balances[pid][type][ym] = {
|
||||||
availableAtMonthStart: Math.max(
|
availableAtMonthStart: totalAvailableAtStart,
|
||||||
0,
|
carryAtStart: carryAtStart,
|
||||||
acquiredBalance - usedBeforeCurrentMonth,
|
usedInMonth: usedInMonth,
|
||||||
),
|
|
||||||
usedInMonth: usageByMonth[pid]?.[type]?.[ym] || 0,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1454,10 +1638,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const type = String(conf.leave_type || conf.type)
|
const type = String(conf.leave_type || conf.type)
|
||||||
.trim()
|
.trim()
|
||||||
.toUpperCase();
|
.toUpperCase();
|
||||||
|
|
||||||
const startBal =
|
const startBal =
|
||||||
this.monthlyLeaveBalances[String(person.id)]?.[type]?.[
|
this.monthlyLeaveBalances[String(person.id)]?.[type]?.[
|
||||||
monthsToDisplay[0]
|
monthsToDisplay[0]
|
||||||
]?.availableAtMonthStart || 0;
|
]?.availableAtMonthStart || 0;
|
||||||
|
const carryAtStart =
|
||||||
|
this.monthlyLeaveBalances[String(person.id)]?.[type]?.[
|
||||||
|
monthsToDisplay[0]
|
||||||
|
]?.carryAtStart || 0;
|
||||||
|
|
||||||
let totalUsed = 0;
|
let totalUsed = 0;
|
||||||
monthsToDisplay.forEach((ym) => {
|
monthsToDisplay.forEach((ym) => {
|
||||||
@@ -1467,20 +1656,37 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const endBal = Math.max(0, startBal - totalUsed);
|
const endBal = Math.max(0, startBal - totalUsed);
|
||||||
|
const remainingCarry = Math.max(0, carryAtStart - totalUsed);
|
||||||
const fmt = (n) =>
|
const fmt = (n) =>
|
||||||
n > 0 ? (Number.isInteger(n) ? n : n.toFixed(1)) : "0";
|
n > 0 ? (Number.isInteger(n) ? n : n.toFixed(2)) : "0";
|
||||||
|
|
||||||
let alertHtml = "";
|
let alertHtml = "";
|
||||||
const cMonth = parseInt(monthsToDisplay[0].split("-")[1], 10);
|
const cMonth = parseInt(monthsToDisplay[0].split("-")[1], 10);
|
||||||
const dateVal = conf.anniversary_date || conf.date;
|
|
||||||
|
|
||||||
if (endBal > 0 && dateVal && dateVal.includes("-")) {
|
const resetMonth = parseInt(
|
||||||
const resetMonth = parseInt(dateVal.split("-")[1], 10) || 1;
|
conf.date ? conf.date.split("-")[1] : 1,
|
||||||
const alertMonth = resetMonth === 1 ? 12 : resetMonth - 1;
|
10,
|
||||||
|
);
|
||||||
|
const deadline = parseInt(conf.carry_over_deadline_month || 12, 10);
|
||||||
|
|
||||||
if (cMonth === alertMonth || cMonth === resetMonth) {
|
const alertMonthEndCycle = resetMonth === 1 ? 12 : resetMonth - 1;
|
||||||
alertHtml = `<div class="fc-burn-alert" title="Alerte : ${fmt(endBal)} jour(s) perdu(s) à la fin du cycle !">🔥</div>`;
|
|
||||||
|
// ⚠️ Alerte 1 : Le Report expire à la fin de ce mois !
|
||||||
|
if (remainingCarry > 0 && cMonth === deadline) {
|
||||||
|
alertHtml = `<div class="fc-burn-alert" title="${tr("fc_alert_burn_jra") || "Alerte : Jours reportés expirent à la fin de ce mois !"}">🔥</div>`;
|
||||||
}
|
}
|
||||||
|
// ⚠️ Alerte 2 : Le solde de base expire à la fin du cycle annuel !
|
||||||
|
else if (
|
||||||
|
endBal > 0 &&
|
||||||
|
(cMonth === alertMonthEndCycle || cMonth === resetMonth)
|
||||||
|
) {
|
||||||
|
const alertMsg =
|
||||||
|
window.I18N && window.I18N["fc_alert_burn_days"]
|
||||||
|
? window.I18N["fc_alert_burn_days"]
|
||||||
|
.replace("%s", fmt(endBal))
|
||||||
|
.replace("%s", "la fin de cycle")
|
||||||
|
: `Alerte : ${fmt(endBal)} j perdu(s) à la fin du cycle !`;
|
||||||
|
alertHtml = `<div class="fc-burn-alert" title="${alertMsg}">🔥</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
cards += `<div class="fc-min-chip" title="Solde: ${fmt(startBal)}"><span class="type">${conf.type || type}</span><span class="val">${fmt(endBal)}</span>${totalUsed > 0 ? `<span class="fc-used-badge">-${fmt(totalUsed)}</span>` : ""}${alertHtml}</div>`;
|
cards += `<div class="fc-min-chip" title="Solde: ${fmt(startBal)}"><span class="type">${conf.type || type}</span><span class="val">${fmt(endBal)}</span>${totalUsed > 0 ? `<span class="fc-used-badge">-${fmt(totalUsed)}</span>` : ""}${alertHtml}</div>`;
|
||||||
@@ -1689,6 +1895,68 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
this.renderMonthCalendar();
|
this.renderMonthCalendar();
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Câblage sécurisé du formulaire d'ajustement de solde
|
||||||
|
const formSnap = document.getElementById("formSnapshot");
|
||||||
|
if (formSnap) {
|
||||||
|
formSnap.addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn =
|
||||||
|
document.getElementById("btnSubmitSnapshot") ||
|
||||||
|
e.target.querySelector('button[type="submit"]');
|
||||||
|
const originalText = btn ? btn.innerHTML : "";
|
||||||
|
if (btn) {
|
||||||
|
btn.innerHTML = "⏳...";
|
||||||
|
btn.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
person_id: document.getElementById("snapPerson").value,
|
||||||
|
leave_type: document.getElementById("snapType").value,
|
||||||
|
snapshot_date: document.getElementById("snapDate").value,
|
||||||
|
remaining_balance: parseFloat(
|
||||||
|
document.getElementById("snapBalance").value,
|
||||||
|
),
|
||||||
|
carry_over_balance: parseFloat(
|
||||||
|
document.getElementById("snapCarryOver").value || 0,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await this.postApi(
|
||||||
|
"/modules/family-calendar/includes/api/save-leave-snapshot.php",
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
if (!res.status || res.status !== "success")
|
||||||
|
throw new Error(res.message || "Erreur");
|
||||||
|
|
||||||
|
if (window.showToast)
|
||||||
|
showToast(
|
||||||
|
tr("fc_settings_updated") || "Solde corrigé !",
|
||||||
|
"success",
|
||||||
|
);
|
||||||
|
|
||||||
|
const modal =
|
||||||
|
document.getElementById("modalSnapshot") ||
|
||||||
|
document.getElementById("snapshotModal");
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = "none";
|
||||||
|
modal.classList.remove("open");
|
||||||
|
}
|
||||||
|
document.body.classList.remove("no-scroll");
|
||||||
|
formSnap.reset();
|
||||||
|
|
||||||
|
await this.refreshAllData(); // Recharge les graphiques en direct
|
||||||
|
} catch (err) {
|
||||||
|
alert(tr("error_occured") + " " + err.message);
|
||||||
|
} finally {
|
||||||
|
if (btn) {
|
||||||
|
btn.innerHTML = originalText;
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseDown(e) {
|
handleMouseDown(e) {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ try {
|
|||||||
$stmtPeople = $pdo->query("SELECT id, name, role, care_modes, color FROM pf_people WHERE is_active = 1 ORDER BY role ASC, name ASC");
|
$stmtPeople = $pdo->query("SELECT id, name, role, care_modes, color FROM pf_people WHERE is_active = 1 ORDER BY role ASC, name ASC");
|
||||||
$people = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
|
$people = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$stmtLeaves = $pdo->query("SELECT person_id, leave_type, anniversary_date, method, allowance FROM pf_person_leave_meta");
|
$stmtLeaves = $pdo->query("SELECT person_id, leave_type, anniversary_date, method, allowance, carry_over_max_days, carry_over_deadline_month FROM pf_person_leave_meta");
|
||||||
$leavesRaw = $stmtLeaves->fetchAll(PDO::FETCH_ASSOC);
|
$leavesRaw = $stmtLeaves->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$leavesMap = [];
|
$leavesMap = [];
|
||||||
@@ -24,7 +24,9 @@ try {
|
|||||||
'type' => $leave['leave_type'],
|
'type' => $leave['leave_type'],
|
||||||
'method' => $leave['method'] ?? 'FIXED',
|
'method' => $leave['method'] ?? 'FIXED',
|
||||||
'allowance' => $leave['allowance'] ?? 0,
|
'allowance' => $leave['allowance'] ?? 0,
|
||||||
'date' => $leave['anniversary_date']
|
'date' => $leave['anniversary_date'],
|
||||||
|
'carry_over_max_days' => $leave['carry_over_max_days'] ?? 0,
|
||||||
|
'carry_over_deadline_month' => $leave['carry_over_deadline_month'] ?? 12
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,8 +39,8 @@ try {
|
|||||||
'calendar_working_hours' => $calendarSettingsRaw['calendar_working_hours'] ?? '08:00-19:00'
|
'calendar_working_hours' => $calendarSettingsRaw['calendar_working_hours'] ?? '08:00-19:00'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Catalogue des congés
|
// Catalogue des congés (Nettoyé des règles de report qui sont maintenant sur la personne)
|
||||||
$stmtLeaveTypes = $pdo->query("SELECT code, label, default_allowance, reset_month, allow_carry_over FROM pf_leave_types ORDER BY label ASC");
|
$stmtLeaveTypes = $pdo->query("SELECT code, label, default_allowance, reset_month FROM pf_leave_types ORDER BY label ASC");
|
||||||
$leaveTypes = $stmtLeaveTypes->fetchAll(PDO::FETCH_ASSOC);
|
$leaveTypes = $stmtLeaveTypes->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
@@ -97,20 +99,22 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($action === 'save_leave_type') {
|
if ($action === 'save_leave_type') {
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception(tr('error_occured'));
|
||||||
|
|
||||||
$mode = $_POST['mode'] ?? 'add';
|
$mode = $_POST['mode'] ?? 'add';
|
||||||
$code = strtoupper(trim($_POST['code'] ?? ''));
|
$code = strtoupper(trim($_POST['code'] ?? ''));
|
||||||
$label = trim($_POST['label'] ?? '');
|
$label = trim($_POST['label'] ?? '');
|
||||||
|
$carryMax = (float)($_POST['carry_over_max_days'] ?? 0);
|
||||||
|
$carryDeadline = (int)($_POST['carry_over_deadline_month'] ?? 12);
|
||||||
|
|
||||||
if (empty($code) || empty($label)) throw new Exception("Le code et le label sont obligatoires.");
|
if (empty($code) || empty($label)) throw new Exception(tr('fc_err_code_label'));
|
||||||
|
|
||||||
if ($mode === 'add') {
|
if ($mode === 'add') {
|
||||||
$stmt = $pdo->prepare("INSERT INTO pf_leave_types (code, label) VALUES (?, ?)");
|
$stmt = $pdo->prepare("INSERT INTO pf_leave_types (code, label, carry_over_max_days, carry_over_deadline_month) VALUES (?, ?, ?, ?)");
|
||||||
$stmt->execute([$code, $label]);
|
$stmt->execute([$code, $label, $carryMax, $carryDeadline]);
|
||||||
} else {
|
} else {
|
||||||
$stmt = $pdo->prepare("UPDATE pf_leave_types SET label = ? WHERE code = ?");
|
$stmt = $pdo->prepare("UPDATE pf_leave_types SET label = ?, carry_over_max_days = ?, carry_over_deadline_month = ? WHERE code = ?");
|
||||||
$stmt->execute([$label, $code]);
|
$stmt->execute([$label, $carryMax, $carryDeadline, $code]);
|
||||||
}
|
}
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
exit;
|
exit;
|
||||||
@@ -141,7 +145,7 @@ try {
|
|||||||
// ─── 5. GESTION DES CONGÉS INDIVIDUELS (MEMBRES) ───
|
// ─── 5. GESTION DES CONGÉS INDIVIDUELS (MEMBRES) ───
|
||||||
if ($action === 'get_person_leaves') {
|
if ($action === 'get_person_leaves') {
|
||||||
$personId = (int)($_GET['person_id'] ?? 0);
|
$personId = (int)($_GET['person_id'] ?? 0);
|
||||||
$stmt = $pdo->prepare("SELECT id, leave_type, allowance, method, anniversary_date FROM pf_person_leave_meta WHERE person_id = ? ORDER BY leave_type ASC");
|
$stmt = $pdo->prepare("SELECT id, leave_type, allowance, method, anniversary_date, carry_over_max_days, carry_over_deadline_month FROM pf_person_leave_meta WHERE person_id = ? ORDER BY leave_type ASC");
|
||||||
$stmt->execute([$personId]);
|
$stmt->execute([$personId]);
|
||||||
echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]);
|
echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]);
|
||||||
exit;
|
exit;
|
||||||
@@ -150,25 +154,35 @@ try {
|
|||||||
if ($action === 'add_person_leave') {
|
if ($action === 'add_person_leave') {
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
||||||
|
|
||||||
|
$id = (int)($_POST['meta_id'] ?? 0); // Nouvel ID pour différencier l'Update de l'Insert
|
||||||
$personId = (int)($_POST['person_id'] ?? 0);
|
$personId = (int)($_POST['person_id'] ?? 0);
|
||||||
$type = strtoupper(trim($_POST['leave_type'] ?? ''));
|
$type = strtoupper(trim($_POST['leave_type'] ?? ''));
|
||||||
$allowance = (float)($_POST['allowance'] ?? 0);
|
$allowance = (float)($_POST['allowance'] ?? 0);
|
||||||
$resetMonth = (int)($_POST['reset_month'] ?? 1);
|
$resetMonth = (int)($_POST['reset_month'] ?? 1);
|
||||||
|
$carryMax = (float)($_POST['carry_over_max_days'] ?? 0);
|
||||||
|
$carryDeadline = (int)($_POST['carry_over_deadline_month'] ?? 12);
|
||||||
|
|
||||||
$method = in_array($_POST['method'] ?? '', ['FIXED', 'ACCUMULATED']) ? $_POST['method'] : 'FIXED';
|
$method = in_array($_POST['method'] ?? '', ['FIXED', 'ACCUMULATED']) ? $_POST['method'] : 'FIXED';
|
||||||
|
|
||||||
if ($personId <= 0 || empty($type)) throw new Exception("Données invalides.");
|
if ($personId <= 0 || empty($type)) throw new Exception("Données invalides.");
|
||||||
|
|
||||||
|
// On construit la date anniversaire avec le mois choisi par l'utilisateur
|
||||||
|
$anniversaryDate = "2000-" . str_pad((string)$resetMonth, 2, "0", STR_PAD_LEFT) . "-01";
|
||||||
|
|
||||||
|
if ($id > 0) {
|
||||||
|
// ✏️ UPDATE (Modification)
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_person_leave_meta SET allowance = ?, method = ?, anniversary_date = ?, carry_over_max_days = ?, carry_over_deadline_month = ? WHERE id = ? AND person_id = ?");
|
||||||
|
$stmt->execute([$allowance, $method, $anniversaryDate, $carryMax, $carryDeadline, $id, $personId]);
|
||||||
|
} else {
|
||||||
|
// ➕ INSERT (Ajout)
|
||||||
// Vérification des doublons
|
// Vérification des doublons
|
||||||
$check = $pdo->prepare("SELECT id FROM pf_person_leave_meta WHERE person_id = ? AND leave_type = ?");
|
$check = $pdo->prepare("SELECT id FROM pf_person_leave_meta WHERE person_id = ? AND leave_type = ?");
|
||||||
$check->execute([$personId, $type]);
|
$check->execute([$personId, $type]);
|
||||||
if ($check->rowCount() > 0) throw new Exception("Ce congé est déjà attribué à cette personne.");
|
if ($check->rowCount() > 0) throw new Exception("Ce congé est déjà attribué à cette personne.");
|
||||||
|
|
||||||
// On construit la date anniversaire avec le mois choisi par l'utilisateur
|
$stmt = $pdo->prepare("INSERT INTO pf_person_leave_meta (person_id, leave_type, allowance, method, anniversary_date, carry_over_max_days, carry_over_deadline_month) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||||
$anniversaryDate = "2000-" . str_pad((string)$resetMonth, 2, "0", STR_PAD_LEFT) . "-01";
|
$stmt->execute([$personId, $type, $allowance, $method, $anniversaryDate, $carryMax, $carryDeadline]);
|
||||||
|
}
|
||||||
$stmt = $pdo->prepare("INSERT INTO pf_person_leave_meta (person_id, leave_type, allowance, method, anniversary_date) VALUES (?, ?, ?, ?, ?)");
|
|
||||||
$stmt->execute([$personId, $type, $allowance, $method, $anniversaryDate]);
|
|
||||||
|
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ require __DIR__ . '/../../../../includes/db.php';
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$stmt = $pdo->query("
|
$stmt = $pdo->query("
|
||||||
SELECT person_id, leave_type, snapshot_date, remaining_balance
|
SELECT person_id, leave_type, snapshot_date, remaining_balance, carry_over_balance
|
||||||
FROM pf_leave_snapshots
|
FROM pf_leave_snapshots
|
||||||
ORDER BY snapshot_date ASC
|
ORDER BY snapshot_date ASC
|
||||||
");
|
");
|
||||||
|
|||||||
@@ -15,13 +15,16 @@ try {
|
|||||||
$stmtDel = $pdo->prepare("DELETE FROM pf_leave_snapshots WHERE person_id = ? AND leave_type = ? AND snapshot_date = ?");
|
$stmtDel = $pdo->prepare("DELETE FROM pf_leave_snapshots WHERE person_id = ? AND leave_type = ? AND snapshot_date = ?");
|
||||||
$stmtDel->execute([$input['person_id'], $input['leave_type'], $input['snapshot_date']]);
|
$stmtDel->execute([$input['person_id'], $input['leave_type'], $input['snapshot_date']]);
|
||||||
|
|
||||||
|
$carry_over_balance = isset($input['carry_over_balance']) ? (float)$input['carry_over_balance'] : 0.00;
|
||||||
|
|
||||||
// Étape 2 : On insère le nouveau solde
|
// Étape 2 : On insère le nouveau solde
|
||||||
$stmt = $pdo->prepare("INSERT INTO pf_leave_snapshots (person_id, leave_type, snapshot_date, remaining_balance) VALUES (?, ?, ?, ?)");
|
$stmt = $pdo->prepare("INSERT INTO pf_leave_snapshots (person_id, leave_type, snapshot_date, remaining_balance, carry_over_balance) VALUES (?, ?, ?, ?, ?)");
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
$input['person_id'],
|
$input['person_id'],
|
||||||
$input['leave_type'],
|
$input['leave_type'],
|
||||||
$input['snapshot_date'],
|
$input['snapshot_date'],
|
||||||
$input['remaining_balance']
|
$input['remaining_balance'],
|
||||||
|
$carry_over_balance
|
||||||
]);
|
]);
|
||||||
|
|
||||||
echo json_encode(['status' => 'success']);
|
echo json_encode(['status' => 'success']);
|
||||||
|
|||||||
Reference in new issue
Block a user