@@ -355,6 +355,17 @@ CREATE TABLE IF NOT EXISTS pf_holidays_ideas (
|
||||
notes TEXT DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pf_holidays_attachments` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`holiday_id` INT NOT NULL,
|
||||
`item_id` INT DEFAULT NULL,
|
||||
`file_name` VARCHAR(255) NOT NULL,
|
||||
`file_path` VARCHAR(255) NOT NULL,
|
||||
`uploaded_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
CONSTRAINT `fk_holiday_attachment` FOREIGN KEY (`holiday_id`) REFERENCES `pf_holidays`(`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pf_geocode_cache (
|
||||
q_hash CHAR(64) PRIMARY KEY,
|
||||
q VARCHAR(255),
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/meta_db.php';
|
||||
|
||||
$db_host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||
$db_user = getenv('DB_USER') ?: 'househub';
|
||||
$db_pass = getenv('DB_PASS') ?: 'househub_dev';
|
||||
|
||||
echo "<h1>🗺️ Migration : Ajout du Véhicule aux Voyages</h1><ul>";
|
||||
|
||||
try {
|
||||
$stmt = $meta_pdo->query("SELECT db_name, name FROM families WHERE is_active = 1");
|
||||
$families = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($families as $family) {
|
||||
$dbName = $family['db_name'];
|
||||
try {
|
||||
$pdo = new PDO("mysql:host=$db_host;dbname=$dbName;charset=utf8mb4", $db_user, $db_pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||
|
||||
// Ajout de la colonne vehicle_id
|
||||
$pdo->exec("ALTER TABLE pf_holidays ADD COLUMN vehicle_id INT DEFAULT NULL");
|
||||
|
||||
// Optionnel mais propre : On ajoute une clé étrangère
|
||||
$pdo->exec("ALTER TABLE pf_holidays ADD CONSTRAINT fk_holiday_vehicle FOREIGN KEY (vehicle_id) REFERENCES pf_vehicles(id) ON DELETE SET NULL");
|
||||
|
||||
echo "<li>✅ {$family['name']} : Colonne vehicle_id ajoutée !</li>";
|
||||
} catch (\PDOException $e) {
|
||||
if ($e->getCode() == '42S21' || strpos($e->getMessage(), 'Duplicate column') !== false) {
|
||||
echo "<li>⏩ {$family['name']} : Déjà à jour.</li>";
|
||||
} else {
|
||||
echo "<li>❌ {$family['name']} : Erreur - " . $e->getMessage() . "</li>";
|
||||
}
|
||||
}
|
||||
}
|
||||
echo "</ul><h2>🎉 Migration terminée !</h2>";
|
||||
} catch (Exception $e) {
|
||||
die("Erreur fatale : " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
// migrate_attachments.php
|
||||
|
||||
// 1. Inclusion de ta connexion BDD principale (Ajuste le chemin si besoin)
|
||||
require __DIR__ . '/includes/db.php';
|
||||
|
||||
echo "<pre>🚀 Lancement de la migration multi-bases...\n\n";
|
||||
|
||||
try {
|
||||
// 2. Récupérer toutes les bases de données qui commencent par "househub_f"
|
||||
$stmt = $pdo->query("SHOW DATABASES LIKE 'househub_f%'");
|
||||
$databases = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if (empty($databases)) {
|
||||
die("❌ Aucune base de données familiale trouvée (Format househub_f1, househub_f2...).\n");
|
||||
}
|
||||
|
||||
$successCount = 0;
|
||||
|
||||
foreach ($databases as $dbName) {
|
||||
// Sécurité : on vérifie le format exact (househub_f suivi de chiffres)
|
||||
if (preg_match('/^househub_f\d+$/', $dbName)) {
|
||||
echo "⚙️ Mise à jour de la base : <strong>$dbName</strong>... ";
|
||||
|
||||
// On bascule la connexion sur la base de la famille
|
||||
$pdo->exec("USE `$dbName`");
|
||||
|
||||
// 3. Création de la table
|
||||
$sql = "CREATE TABLE IF NOT EXISTS pf_holidays_attachments (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
holiday_id INT NOT NULL,
|
||||
item_id INT DEFAULT NULL,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
file_path VARCHAR(255) NOT NULL,
|
||||
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_holiday_attachment FOREIGN KEY (holiday_id) REFERENCES pf_holidays(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;";
|
||||
|
||||
$pdo->exec($sql);
|
||||
echo "✅ OK\n";
|
||||
$successCount++;
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n🎉 Migration terminée avec succès sur $successCount base(s) familiale(s) !</pre>";
|
||||
|
||||
} catch (PDOException $e) {
|
||||
die("\n❌ Erreur SQL pendant la migration : " . $e->getMessage() . "</pre>");
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Script de rattrapage : Configuration dynamique des congés (CP, JRA, JA)
|
||||
* Ce script lit l'historique réel des soldes/snapshots pour pré-configurer la modale Settings.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/includes/meta_db.php';
|
||||
|
||||
$db_host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||
$db_user = getenv('DB_USER') ?: 'househub';
|
||||
$db_pass = getenv('DB_PASS') ?: 'househub_dev';
|
||||
|
||||
echo "<h1>🚀 Début de la restauration des compteurs de congés</h1>";
|
||||
|
||||
try {
|
||||
$stmt = $meta_pdo->query("SELECT db_name, name FROM families WHERE is_active = 1");
|
||||
$families = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($families as $family) {
|
||||
$dbName = $family['db_name'];
|
||||
echo "<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. Création de la table de configuration (si elle n'existe pas)
|
||||
$pdo->exec("
|
||||
CREATE TABLE IF NOT EXISTS pf_person_leave_meta (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
person_id INT NOT NULL,
|
||||
leave_type VARCHAR(50) NOT NULL,
|
||||
anniversary_date DATE NOT NULL,
|
||||
UNIQUE KEY uq_person_leave (person_id, leave_type),
|
||||
FOREIGN KEY (person_id) REFERENCES pf_people(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
");
|
||||
|
||||
// 2. On scanne l'historique des soldes et snapshots pour trouver TOUS les types existants
|
||||
$stmtLeaves = $pdo->query("
|
||||
SELECT DISTINCT person_id, leave_type
|
||||
FROM (
|
||||
SELECT person_id, leave_type FROM pf_leave_balances
|
||||
UNION
|
||||
SELECT person_id, leave_type FROM pf_leave_snapshots
|
||||
) as combined
|
||||
WHERE leave_type IS NOT NULL AND leave_type != ''
|
||||
");
|
||||
|
||||
$existingLeaves = $stmtLeaves->fetchAll(PDO::FETCH_ASSOC);
|
||||
$inserted = 0;
|
||||
|
||||
$stmtInsert = $pdo->prepare("
|
||||
INSERT IGNORE INTO pf_person_leave_meta (person_id, leave_type, anniversary_date)
|
||||
VALUES (?, ?, ?)
|
||||
");
|
||||
|
||||
// 3. On injecte les données dans la configuration avec les dates par défaut de ton ancien JS
|
||||
foreach ($existingLeaves as $leave) {
|
||||
$pid = $leave['person_id'];
|
||||
$type = strtoupper(trim($leave['leave_type']));
|
||||
|
||||
// Dates d'anniversaire par défaut (année 2000 arbitraire, seul le mois/jour compte)
|
||||
$anniversary = '2000-01-01';
|
||||
if ($type === 'CP') $anniversary = '2000-06-01'; // 1er Juin
|
||||
if ($type === 'JRA') $anniversary = '2000-01-01'; // 1er Janvier
|
||||
if ($type === 'JA') $anniversary = '2000-04-29'; // 29 Avril (issu de ton ancien JS)
|
||||
|
||||
$stmtInsert->execute([$pid, $type, $anniversary]);
|
||||
if ($stmtInsert->rowCount() > 0) {
|
||||
$inserted++;
|
||||
}
|
||||
}
|
||||
|
||||
echo "✅ $inserted compteurs historiques (CP, JRA, JA...) détectés et configurés avec succès pour la modale.<br>";
|
||||
|
||||
} catch (PDOException $e) {
|
||||
echo "❌ Erreur sur la base $dbName : " . $e->getMessage() . "<br>";
|
||||
}
|
||||
}
|
||||
|
||||
echo "<h1>🎉 Restauration terminée avec succès !</h1>";
|
||||
|
||||
} catch (Exception $e) {
|
||||
die("❌ Erreur fatale Meta DB : " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
@@ -1161,3 +1161,219 @@ function launchGpsApp(app) {
|
||||
closeGpsModal();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GESTION DU PORTE-DOCUMENTS (UPLOAD)
|
||||
// ============================================================================
|
||||
window.currentDocsStepId = null;
|
||||
|
||||
// 🔥 On attache le verrou à window pour éviter les erreurs de redéclaration
|
||||
window.isUploadingDocs = window.isUploadingDocs || false;
|
||||
|
||||
function openDocsModal(sortOrder) {
|
||||
window.currentDocsStepId = sortOrder;
|
||||
document.getElementById("docsModal").style.display = "flex";
|
||||
document.body.classList.add("no-scroll");
|
||||
document.getElementById("uploadStatus").innerHTML = "";
|
||||
|
||||
const listContainer = document.getElementById("docsListContainer");
|
||||
listContainer.innerHTML =
|
||||
'<p style="text-align: center; font-size: 0.85rem; color: var(--text-muted);">⏳ Chargement des documents...</p>';
|
||||
|
||||
const holidayId = document.querySelector('input[name="holiday_id"]').value;
|
||||
|
||||
// 🔥 On va chercher les documents existants !
|
||||
fetch(
|
||||
`/modules/holidays/includes/api/get_attachments.php?holiday_id=${holidayId}&item_id=${sortOrder}`,
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
listContainer.innerHTML = ""; // On vide le message de chargement
|
||||
|
||||
if (data.success && data.files.length > 0) {
|
||||
data.files.forEach((f) => {
|
||||
// On rend le nom du fichier cliquable pour ouvrir le document dans un nouvel onglet
|
||||
const docHtml = `
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; padding: 10px; background: var(--bg-page); border: 1px solid var(--border-light); border-radius: 6px; margin-bottom: 6px;">
|
||||
<div style="display: flex; align-items: center; gap: 10px; overflow: hidden; cursor: pointer;" onclick="window.open('/${f.file_path}', '_blank')">
|
||||
<span style="font-size: 1.2rem;">📄</span>
|
||||
<span style="font-size: 0.9rem; color: var(--primary); font-weight: bold; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" title="Ouvrir ${f.file_name}">${f.file_name}</span>
|
||||
</div>
|
||||
<button type="button" onclick="deleteAttachment(${f.id}, this)" style="background: none; border: none; color: var(--danger); cursor: pointer; font-size: 1rem;" title="Supprimer">🗑️</button>
|
||||
</div>
|
||||
`;
|
||||
listContainer.insertAdjacentHTML("beforeend", docHtml);
|
||||
});
|
||||
} else {
|
||||
listContainer.innerHTML =
|
||||
'<p style="text-align: center; font-size: 0.85rem; color: var(--text-muted); font-style: italic;">Aucun document pour cette étape.</p>';
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
listContainer.innerHTML =
|
||||
'<p style="text-align: center; color: var(--danger);">Erreur lors du chargement.</p>';
|
||||
});
|
||||
}
|
||||
|
||||
function closeDocsModal() {
|
||||
document.getElementById("docsModal").style.display = "none";
|
||||
document.body.classList.remove("no-scroll");
|
||||
}
|
||||
|
||||
function handleFileUpload(input) {
|
||||
// 1. LE VERROU : Si un envoi est déjà en cours, on bloque tout !
|
||||
if (window.isUploadingDocs) return;
|
||||
|
||||
if (!input.files || input.files.length === 0) return;
|
||||
|
||||
// On ferme le verrou
|
||||
window.isUploadingDocs = true;
|
||||
|
||||
const file = input.files[0];
|
||||
const holidayId = document.querySelector('input[name="holiday_id"]').value;
|
||||
const statusDiv = document.getElementById("uploadStatus");
|
||||
const listContainer = document.getElementById("docsListContainer");
|
||||
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
statusDiv.innerHTML =
|
||||
"<span style='color: var(--danger);'>Fichier trop lourd (Max 5Mo).</span>";
|
||||
input.value = "";
|
||||
window.isUploadingDocs = false; // On rouvre le verrou
|
||||
return;
|
||||
}
|
||||
|
||||
statusDiv.innerHTML =
|
||||
"<span style='color: var(--primary);'>⏳ Envoi en cours...</span>";
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append("holiday_id", holidayId);
|
||||
fd.append("item_id", window.currentDocsStepId);
|
||||
fd.append("file", file);
|
||||
|
||||
fetch("/modules/holidays/includes/api/upload_attachment.php", {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.success) {
|
||||
statusDiv.innerHTML = `<span style='color: var(--success);'>✅ Sauvegardé !</span>`;
|
||||
|
||||
const emptyMsg = listContainer.querySelector("p");
|
||||
if (emptyMsg) emptyMsg.remove();
|
||||
|
||||
const docHtml = `
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; padding: 10px; background: var(--bg-page); border: 1px solid var(--border-light); border-radius: 6px; margin-bottom: 6px;">
|
||||
<div style="display: flex; align-items: center; gap: 10px; overflow: hidden;">
|
||||
<span style="font-size: 1.2rem;">📄</span>
|
||||
<span style="font-size: 0.9rem; color: var(--text-main); white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" title="${data.file_name}">${data.file_name}</span>
|
||||
</div>
|
||||
<button type="button" onclick="deleteAttachment(${data.id}, this)" style="background: none; border: none; color: var(--danger); cursor: pointer; font-size: 1rem;" title="Supprimer">🗑️</button>
|
||||
</div>
|
||||
`;
|
||||
listContainer.insertAdjacentHTML("beforeend", docHtml);
|
||||
} else {
|
||||
statusDiv.innerHTML = `<span style='color: var(--danger);'>❌ Erreur: ${data.error}</span>`;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
statusDiv.innerHTML =
|
||||
"<span style='color: var(--danger);'>❌ Erreur réseau.</span>";
|
||||
})
|
||||
.finally(() => {
|
||||
input.value = "";
|
||||
// 🔥 2. On rouvre le verrou SEULEMENT quand tout est terminé
|
||||
setTimeout(() => {
|
||||
window.isUploadingDocs = false;
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
// Fonction pour supprimer un document
|
||||
function deleteAttachment(fileId, btnElement) {
|
||||
if (!confirm("Voulez-vous vraiment supprimer ce document définitivement ?"))
|
||||
return;
|
||||
|
||||
const holidayId = document.querySelector('input[name="holiday_id"]').value;
|
||||
const row = btnElement.closest('div[style*="border: 1px solid"]'); // Cible la ligne d'affichage
|
||||
row.style.opacity = "0.4"; // Effet visuel d'attente
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append("file_id", fileId);
|
||||
fd.append("holiday_id", holidayId);
|
||||
|
||||
fetch("/modules/holidays/includes/api/delete_attachment.php", {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.success) {
|
||||
row.remove(); // On efface la ligne
|
||||
|
||||
// Si c'était le dernier fichier, on remet le texte "Aucun document"
|
||||
const listContainer = document.getElementById("docsListContainer");
|
||||
if (listContainer.children.length === 0) {
|
||||
listContainer.innerHTML =
|
||||
'<p style="text-align: center; font-size: 0.85rem; color: var(--text-muted); font-style: italic;">Aucun document pour cette étape.</p>';
|
||||
}
|
||||
} else {
|
||||
alert("Erreur : " + data.error);
|
||||
row.style.opacity = "1";
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
alert("Erreur réseau lors de la suppression.");
|
||||
row.style.opacity = "1";
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GÉNÉRATION DU CARNET DE VOYAGE (PDF Côté Client) - VERSION TEXTE BRUT
|
||||
// ============================================================================
|
||||
window.generateTravelBook = function () {
|
||||
const element = document.getElementById("travelBookTemplate");
|
||||
const btn = document.querySelector('button[onclick="generateTravelBook()"]');
|
||||
|
||||
if (!element) {
|
||||
alert("Erreur : Le modèle de carnet de voyage est introuvable.");
|
||||
return;
|
||||
}
|
||||
|
||||
const originalText = btn.innerHTML;
|
||||
btn.innerHTML = "⏳ Génération...";
|
||||
btn.disabled = true;
|
||||
|
||||
// Options ajustées avec un fond blanc forcé
|
||||
const opt = {
|
||||
margin: 10, // 10mm de marge
|
||||
filename: "Carnet_de_Route.pdf",
|
||||
image: { type: "jpeg", quality: 0.98 },
|
||||
html2canvas: { scale: 2, useCORS: true, backgroundColor: "#ffffff" },
|
||||
jsPDF: { unit: "mm", format: "a4", orientation: "portrait" },
|
||||
};
|
||||
|
||||
// 🔥 L'ASTUCE MAGIQUE :
|
||||
// On extrait le HTML en texte brut et on l'encapsule dans un bloc 100% blanc.
|
||||
// Plus aucun conflit possible avec l'affichage de ta page web !
|
||||
const htmlString = `
|
||||
<div style="background-color: #ffffff; color: #000000; width: 100%;">
|
||||
${element.outerHTML}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Génération directe depuis la chaîne de texte
|
||||
html2pdf()
|
||||
.set(opt)
|
||||
.from(htmlString)
|
||||
.save()
|
||||
.then(() => {
|
||||
btn.innerHTML = originalText;
|
||||
btn.disabled = false;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Erreur html2pdf:", err);
|
||||
btn.innerHTML = originalText;
|
||||
btn.disabled = false;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
// modules/holidays/includes/api/delete_attachment.php
|
||||
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||
require_login();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['success' => false, 'error' => 'Méthode non autorisée']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$file_id = isset($_POST['file_id']) ? (int)$_POST['file_id'] : 0;
|
||||
$holiday_id = isset($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : 0;
|
||||
|
||||
if ($file_id === 0 || $holiday_id === 0) {
|
||||
echo json_encode(['success' => false, 'error' => 'Paramètres manquants']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 1. On récupère le chemin exact du fichier pour le supprimer du NAS
|
||||
$stmt = $pdo->prepare("SELECT file_path FROM pf_holidays_attachments WHERE id = ? AND holiday_id = ?");
|
||||
$stmt->execute([$file_id, $holiday_id]);
|
||||
$file = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($file) {
|
||||
$absolutePath = dirname(__DIR__, 4) . '/' . $file['file_path'];
|
||||
|
||||
// 2. On supprime physiquement le fichier s'il existe
|
||||
if (file_exists($absolutePath)) {
|
||||
unlink($absolutePath);
|
||||
}
|
||||
|
||||
// 3. On nettoie la base de données
|
||||
$del = $pdo->prepare("DELETE FROM pf_holidays_attachments WHERE id = ?");
|
||||
$del->execute([$file_id]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => 'Fichier introuvable ou non autorisé']);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
// modules/holidays/includes/api/get_attachments.php
|
||||
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||
require_login();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$holiday_id = isset($_GET['holiday_id']) ? (int)$_GET['holiday_id'] : 0;
|
||||
$item_id = isset($_GET['item_id']) ? (int)$_GET['item_id'] : 0;
|
||||
|
||||
if ($holiday_id === 0) {
|
||||
echo json_encode(['success' => false, 'error' => 'ID manquant']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// On récupère les documents de cette étape spécifique
|
||||
$stmt = $pdo->prepare("SELECT id, file_name, file_path FROM pf_holidays_attachments WHERE holiday_id = ? AND item_id = ? ORDER BY uploaded_at DESC");
|
||||
$stmt->execute([$holiday_id, $item_id]);
|
||||
$files = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode(['success' => true, 'files' => $files]);
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
// modules/holidays/includes/api/upload_attachment.php
|
||||
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||
require_login();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['success' => false, 'error' => 'Méthode non autorisée']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$holiday_id = isset($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : 0;
|
||||
$item_id = (isset($_POST['item_id']) && (int)$_POST['item_id'] > 0) ? (int)$_POST['item_id'] : null;
|
||||
|
||||
if ($holiday_id === 0 || empty($_FILES['file'])) {
|
||||
echo json_encode(['success' => false, 'error' => 'Données manquantes ou fichier invalide']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$file = $_FILES['file'];
|
||||
|
||||
// On range les fichiers dans un sous-dossier par voyage pour rester propre sur le NAS
|
||||
$uploadDir = dirname(__DIR__, 4) . '/uploads/holidays/' . $holiday_id . '/';
|
||||
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0777, true);
|
||||
}
|
||||
|
||||
// Sécurisation du nom de fichier (retrait des accents et caractères spéciaux)
|
||||
$fileName = basename($file['name']);
|
||||
$safeFileName = preg_replace("/[^a-zA-Z0-9.-]/", "_", $fileName);
|
||||
$uniqueFileName = time() . '_' . $safeFileName;
|
||||
$destination = $uploadDir . $uniqueFileName;
|
||||
|
||||
if (move_uploaded_file($file['tmp_name'], $destination)) {
|
||||
// Enregistrement en base de données
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_holidays_attachments (holiday_id, item_id, file_name, file_path) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$holiday_id, $item_id, $fileName, 'uploads/holidays/' . $holiday_id . '/' . $uniqueFileName]);
|
||||
|
||||
$attachmentId = $pdo->lastInsertId();
|
||||
echo json_encode(['success' => true, 'id' => $attachmentId, 'file_name' => $fileName]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => 'Erreur lors de l\'écriture du fichier sur le NAS']);
|
||||
}
|
||||
@@ -90,6 +90,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||
<div class="pf-holidays-detail">
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 20px; flex-wrap: wrap; gap: 15px;">
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 12px;">
|
||||
<a href="?tab=list" class="pf-btn btn-secondary pf-btn-small" style="width: fit-content; text-decoration: none;"><?= tr('btn_back') ?></a>
|
||||
<div style="display: flex; align-items: center; gap: 15px;">
|
||||
@@ -98,14 +99,20 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
|
||||
<script id="holidayDataJson" type="application/json">
|
||||
<?= json_encode(['main' => $holiday, 'items' => $generalItems], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>
|
||||
</script>
|
||||
|
||||
<button type="button" class="pf-btn btn-primary pf-btn-small" onclick="generateTravelBook()" style="display: flex; align-items: center; gap: 6px;">
|
||||
📖 Carnet de Voyage
|
||||
</button>
|
||||
|
||||
<button type="button" class="pf-btn btn-secondary pf-btn-small" onclick="editHoliday(JSON.parse(document.getElementById('holidayDataJson').textContent))">
|
||||
<?= tr('btn_edit_bases') ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="hol-summary-card">
|
||||
@@ -303,6 +310,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||
style="width:26px!important; height:26px!important; font-size:0.8rem; min-width:26px!important; min-height:26px!important;">
|
||||
🧭
|
||||
</button>
|
||||
<button onclick="openDocsModal(<?= $step['sort_order'] ?>)" class="btn-icon-small" title="Documents & Billets" style="width:26px!important; height:26px!important; font-size:0.8rem; min-width:26px!important; min-height:26px!important;">📎</button>
|
||||
|
||||
<button onclick='openPlanningModal(<?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="<?= tr('hdl_view_planning') ?>" style="width:26px!important; height:26px!important; font-size:0.8rem; min-width:26px!important; min-height:26px!important;">📅</button>
|
||||
<button onclick='openCheckpointModal("edit", <?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="<?= tr('btn_edit') ?>" style="width:26px!important; height:26px!important; font-size:0.8rem; min-width:26px!important; min-height:26px!important;">✏️</button>
|
||||
@@ -354,6 +362,8 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div id="checkpointModal" class="pf-modal">
|
||||
<div class="pf-modal-content" style="max-width: 600px;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||||
@@ -498,6 +508,30 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="docsModal" class="pf-modal">
|
||||
<div class="pf-modal-content" style="max-width: 450px; padding: 20px;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; border-bottom: 1px solid var(--border-light); padding-bottom: 10px;">
|
||||
<h3 style="margin:0; font-size: 1.2rem; color: var(--text-main);">📎 Porte-documents</h3>
|
||||
<button type="button" onclick="closeDocsModal()" class="pf-modal-close">×</button>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; padding: 20px; background: var(--bg-page); border: 2px dashed var(--border-light); border-radius: 8px; margin-bottom: 15px;">
|
||||
<p style="margin-top:0; color: var(--text-muted); font-size: 0.9rem;">Ajoutez vos billets, réservations ou PDFs pour cette étape.</p>
|
||||
|
||||
<input type="file" id="docFileInput" style="display: none;" accept=".pdf,.png,.jpg,.jpeg" onchange="handleFileUpload(this)">
|
||||
|
||||
<button type="button" class="pf-btn btn-primary" onclick="document.getElementById('docFileInput').click()" style="margin: 10px 0;">
|
||||
+ Sélectionner un fichier
|
||||
</button>
|
||||
<div id="uploadStatus" style="font-size: 0.85rem; font-weight: bold; margin-top: 10px;"></div>
|
||||
</div>
|
||||
|
||||
<div id="docsListContainer" style="display: flex; flex-direction: column; gap: 8px;">
|
||||
<p style="text-align: center; font-size: 0.85rem; color: var(--text-muted); font-style: italic;">Aucun document pour cette étape.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include __DIR__ . '/modal.php'; ?>
|
||||
|
||||
<script>
|
||||
@@ -561,4 +595,8 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||
|
||||
</script>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/pdf_template.php'; ?>
|
||||
|
||||
<script src="/modules/holidays/holidays.js?v=<?= time() ?>"></script>
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
// Calcul du budget global pour la page de synthèse
|
||||
$totalGeneral = $holiday['budget_food'] + $holiday['budget_extra'];
|
||||
foreach ($generalItems as $gi) {
|
||||
$totalGeneral += $gi['amount'];
|
||||
}
|
||||
$totalSteps = 0;
|
||||
foreach ($steps as $step) {
|
||||
$totalSteps += $step['total_amount'];
|
||||
}
|
||||
$grandTotal = $totalGeneral + $totalSteps;
|
||||
?>
|
||||
|
||||
<div style="display: none;">
|
||||
<div id="travelBookTemplate" style="padding: 40px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #1e293b; background: white; width: 100%; max-width: 800px; margin: 0 auto;">
|
||||
|
||||
<div style="text-align: center; padding-top: 50px; margin-bottom: 60px;">
|
||||
<div style="font-size: 4rem; margin-bottom: 20px;">🗺️</div>
|
||||
<h1 style="font-size: 2.5rem; color: #0f172a; margin-bottom: 10px;"><?= htmlspecialchars($holiday['title'] ?? $holiday['name'] ?? 'Mon Voyage') ?></h1>
|
||||
<h2 style="font-size: 1.5rem; color: #64748b; font-weight: normal; margin-top: 0;">
|
||||
<?php if (!empty($holiday['start_date']) && !empty($holiday['end_date'])): ?>
|
||||
Du <?= date('d/m/Y', strtotime($holiday['start_date'])) ?> au <?= date('d/m/Y', strtotime($holiday['end_date'])) ?>
|
||||
<?php else: ?>
|
||||
Dates à définir
|
||||
<?php endif; ?>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 20px; margin-bottom: 40px; page-break-inside: avoid;">
|
||||
<h3 style="margin-top: 0; color: #0f172a; border-bottom: 2px solid #cbd5e1; padding-bottom: 10px;">💰 Budget Prévisionnel : <?= number_format($grandTotal, 2, ',', ' ') ?> €</h3>
|
||||
<table style="width: 100%; border-collapse: collapse; margin-top: 15px;">
|
||||
<tr>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #e2e8f0; color: #475569;">Frais Généraux (Vols, Locations globales...)</td>
|
||||
<td style="padding: 8px 0; border-bottom: 1px solid #e2e8f0; text-align: right; font-weight: bold;"><?= number_format($totalGeneral, 2, ',', ' ') ?> €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px 0; color: #475569;">Étapes (Hébergements, Activités...)</td>
|
||||
<td style="padding: 8px 0; text-align: right; font-weight: bold;"><?= number_format($totalSteps, 2, ',', ' ') ?> €</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($generalItems) || $holiday['budget_food'] > 0 || $holiday['budget_extra'] > 0): ?>
|
||||
<div style="margin-bottom: 40px; page-break-inside: avoid;">
|
||||
<h3 style="color: #0f172a; border-bottom: 2px solid #0ea5e9; padding-bottom: 5px;">🌍 Réservations & Frais Généraux</h3>
|
||||
<ul style="list-style-type: none; padding-left: 0; margin-top: 15px;">
|
||||
<?php if ($holiday['budget_food'] > 0): ?>
|
||||
<li style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">🍔 Budget Nourriture : <strong><?= number_format($holiday['budget_food'], 2, ',', ' ') ?> €</strong></li>
|
||||
<?php endif; ?>
|
||||
<?php if ($holiday['budget_extra'] > 0): ?>
|
||||
<li style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">🎁 Extras & Souvenirs : <strong><?= number_format($holiday['budget_extra'], 2, ',', ' ') ?> €</strong></li>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($generalItems as $gi):
|
||||
$icon = match($gi['category']) { 'transport' => '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' };
|
||||
?>
|
||||
<li style="padding: 8px 0; border-bottom: 1px solid #f1f5f9; display: flex; justify-content: space-between;">
|
||||
<span><?= $icon ?> <?= htmlspecialchars($gi['name']) ?></span>
|
||||
<span><strong><?= number_format($gi['amount'], 2, ',', ' ') ?> €</strong> <span style="font-size: 0.85em; color: #64748b;"><?= $gi['is_paid'] ? '(Payé ✓)' : '(À payer ⏳)' ?></span></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="page-break-before: always;"></div>
|
||||
<h2 style="color: #0f172a; font-size: 2rem; border-bottom: 3px solid #0f172a; padding-bottom: 10px; margin-bottom: 30px;">📍 Itinéraire Détaillé</h2>
|
||||
|
||||
<?php foreach ($steps as $index => $step): ?>
|
||||
|
||||
<?php if ($index > 0): ?>
|
||||
<div style="text-align: center; padding: 10px 0; color: #94a3b8; page-break-inside: avoid;">
|
||||
<div style="border-left: 2px dashed #cbd5e1; height: 25px; margin: 0 auto; width: 2px;"></div>
|
||||
<div style="margin: 5px 0; font-size: 0.9rem;">🚗 <em>En route vers l'étape suivante...</em></div>
|
||||
<div style="border-left: 2px dashed #cbd5e1; height: 25px; margin: 0 auto; width: 2px;"></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="background: #ffffff; border: 1px solid #e2e8f0; border-left: 6px solid #0ea5e9; border-radius: 8px; padding: 20px; margin-bottom: 0px; page-break-inside: avoid;">
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 15px;">
|
||||
<div>
|
||||
<h3 style="margin: 0; font-size: 1.4rem; color: #0f172a;"><?= htmlspecialchars($step['location_name']) ?></h3>
|
||||
<?php if (!empty($step['step_start_date']) && !empty($step['step_end_date'])): ?>
|
||||
<div style="color: #0ea5e9; margin-top: 5px; font-weight: bold; font-size: 0.95rem;">
|
||||
📅 Du <?= date('d/m', strtotime($step['step_start_date'])) ?> au <?= date('d/m', strtotime($step['step_end_date'])) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<span style="background: #f1f5f9; padding: 5px 10px; border-radius: 6px; font-weight: bold; color: #0f172a;">
|
||||
<?= number_format($step['total_amount'], 2, ',', ' ') ?> €
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php $checkpoints = isset($step['items']) ? $step['items'] : []; ?>
|
||||
|
||||
<?php if (!empty($checkpoints)): ?>
|
||||
<div style="margin-top: 15px; border-top: 1px solid #f8fafc; padding-top: 15px;">
|
||||
<h4 style="margin-top: 0; margin-bottom: 10px; color: #475569; font-size: 1rem;">📋 Planning & Activités :</h4>
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem;">
|
||||
<?php foreach ($checkpoints as $cp):
|
||||
$cpIcon = match($cp['category']) { 'transport' => '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' };
|
||||
// Sécurité sur la date d'activité (item_date ou date)
|
||||
$cpDate = !empty($cp['item_date']) ? $cp['item_date'] : (!empty($cp['date']) ? $cp['date'] : null);
|
||||
?>
|
||||
<tr>
|
||||
<td style="padding: 6px 0; border-bottom: 1px solid #f8fafc; color: #64748b; width: 80px;">
|
||||
<?= $cpDate ? date('d/m', strtotime($cpDate)) : '---' ?>
|
||||
</td>
|
||||
<td style="padding: 6px 0; border-bottom: 1px solid #f8fafc; color: #334155;">
|
||||
<?= $cpIcon ?> <?= htmlspecialchars($cp['name']) ?>
|
||||
</td>
|
||||
<td style="padding: 6px 0; border-bottom: 1px solid #f8fafc; text-align: right; font-weight: bold; color: #0f172a;">
|
||||
<?= number_format($cp['amount'], 2, ',', ' ') ?> €
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<div style="page-break-before: always;"></div>
|
||||
<h2 style="color: #0f172a; font-size: 2rem; border-bottom: 3px solid #0f172a; padding-bottom: 10px; margin-bottom: 30px;">📝 Notes & Informations utiles</h2>
|
||||
|
||||
<p style="color: #64748b; margin-bottom: 40px; font-style: italic;">Espace réservé pour vos numéros d'urgence, codes de cadenas, adresses locales...</p>
|
||||
|
||||
<?php for ($i = 0; $i < 15; $i++): ?>
|
||||
<div style="border-bottom: 1px dotted #cbd5e1; height: 35px; width: 100%;"></div>
|
||||
<?php endfor; ?>
|
||||
|
||||
<div style="text-align: center; margin-top: 60px; font-size: 0.85rem; color: #94a3b8;">
|
||||
Carnet de route généré automatiquement par <strong>HouseHub</strong>. Bon voyage ! ✈️
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user