feat(ucp): onglet Notice & Codes de fonction avec fiche PDF imprimable (v2.5.3)

This commit is contained in:
sorcier committed 2026-11-11 19:57:49 +01:00
1 parent 2a7bb2a122
commit d618d426e7
5 files changed
+289 -3

No files matched your search

+18
View File
@@ -744,6 +744,24 @@ class UcpManager
return is_file($full) && is_readable($full) ? $full : null;
}
/**
* Retourne les codes de fonctions téléphoniques actifs pour la notice UCP.
*/
public static function getFeatureCodesForUser(PDO $db): array
{
try {
$stmt = $db->query('
SELECT feature_key, code, default_code, name, category, description
FROM feature_codes
WHERE enabled = 1
ORDER BY id ASC
');
return $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
} catch (\Throwable) {
return [];
}
}
// =========================================================================
// HELPERS VALIDATION
// =========================================================================
+1
View File
@@ -116,6 +116,7 @@ try {
'csrf_token' => UcpAuth::getCsrfToken(),
'extensions' => UcpManager::getExtensionsState($db, $allowedExts),
'forward_targets' => UcpManager::getForwardTargets($db),
'feature_codes' => UcpManager::getFeatureCodesForUser($db),
],
]);
break;
+155 -1
View File
@@ -4,6 +4,7 @@ let ucpState = {
user: null,
extensions: [],
forward_targets: [],
feature_codes: [],
voicemailsByExt: {},
selectedVmExt: '',
vmFolderFilter: 'all',
@@ -152,6 +153,8 @@ function switchUcpTab(tabName) {
loadPersonalCdr();
} else if (tabName === 'phonebook') {
loadPhonebook();
} else if (tabName === 'guide') {
renderFeatureGuide();
}
}
@@ -163,6 +166,7 @@ async function refreshUcpState(manual = false) {
ucpState.user = res.data.user;
ucpState.extensions = res.data.extensions || [];
ucpState.forward_targets = res.data.forward_targets || [];
ucpState.feature_codes = res.data.feature_codes || [];
if (res.data.csrf_token) {
window.UCP_CSRF_TOKEN = res.data.csrf_token;
}
@@ -177,11 +181,16 @@ async function refreshUcpState(manual = false) {
renderExtensionsGrid();
renderVmExtPills();
syncVmPrefsForm();
renderFeatureGuide();
const accExts = document.getElementById('ucp-account-exts');
if (accExts) {
accExts.textContent = ucpState.extensions.map(e => `${e.extension} (${e.name})`).join(', ') || 'Aucun poste lié';
}
const guideExts = document.getElementById('ucp-guide-user-exts');
if (guideExts) {
guideExts.textContent = ucpState.extensions.map(e => e.extension).join(', ') || 'Aucun';
}
if (manual) {
showUcpToast('État actualisé.', 'info');
@@ -788,7 +797,152 @@ if (profileEmailForm) {
}
// -----------------------------------------------------------------------------
// 5. CHANGEMENT DE MOT DE PASSE UCP
// 5. NOTICE D'UTILISATION, CODES DE FONCTION & EXPORT PDF
// -----------------------------------------------------------------------------
function printFeatureGuide() {
if (ucpState.activeTab !== 'guide') {
switchUcpTab('guide');
}
const dateEl = document.getElementById('ucp-guide-print-date');
if (dateEl) {
dateEl.textContent = new Date().toLocaleDateString('fr-FR');
}
setTimeout(() => window.print(), 80);
}
function renderFeatureGuide() {
const quickBox = document.getElementById('ucp-guide-quickcards');
const catBox = document.getElementById('ucp-guide-categories');
if (!quickBox || !catBox) return;
const allCodes = ucpState.feature_codes || [];
const byKey = new Map(allCodes.map(c => [String(c.feature_key), c]));
const codeOf = (key, fallback = '') => escapeHtml(byKey.get(key)?.code || fallback);
// 1. Encarts "Notice pratique pas à pas"
const vmOwn = byKey.get('voicemail_own');
const vmOther = byKey.get('voicemail_general');
const cfOn = byKey.get('cf_activate');
const cfOff = byKey.get('cf_deactivate');
const cfuOn = byKey.get('cfu_activate');
const dndTog = byKey.get('dnd_toggle');
const dndOn = byKey.get('dnd_activate');
const pkBlf = byKey.get('pickup_blf');
const pkGrp = byKey.get('pickup_group');
quickBox.innerHTML = `
<div class="ucp-print-avoid-break rounded-2xl border border-indigo-200 bg-indigo-50/70 p-4 space-y-2">
<div class="flex items-center gap-2 text-xs font-bold text-indigo-900 uppercase tracking-wide">
<span>📬</span>
<span>Utiliser la Messagerie Vocale</span>
</div>
<ul class="text-[11px] text-slate-700 space-y-1.5 leading-relaxed">
${vmOwn ? `<li>• Depuis votre poste : composez <strong class="font-mono px-1.5 py-0.5 rounded bg-white border border-indigo-200 text-indigo-700">${codeOf('voicemail_own')}</strong> (accès direct sans PIN).</li>` : ''}
${vmOther ? `<li>• Depuis un autre poste : composez <strong class="font-mono px-1.5 py-0.5 rounded bg-white border border-indigo-200 text-indigo-700">${codeOf('voicemail_general')}</strong> + n° de poste + code PIN.</li>` : ''}
<li>• Menu vocal : <strong>1</strong> Écouter • <strong>2</strong> Changer de dossier • <strong>3</strong> Options / Supprimer.</li>
<li>• Depuis l'UCP : écoutez, téléchargez (.wav) et changez votre PIN sans appeler.</li>
</ul>
</div>
<div class="ucp-print-avoid-break rounded-2xl border border-amber-200 bg-amber-50/70 p-4 space-y-2">
<div class="flex items-center gap-2 text-xs font-bold text-amber-900 uppercase tracking-wide">
<span>↪️</span>
<span>Gérer Renvois &amp; Ne Pas Déranger</span>
</div>
<ul class="text-[11px] text-slate-700 space-y-1.5 leading-relaxed">
${cfOn ? `<li>• Renvoi immédiat : composez <strong class="font-mono px-1.5 py-0.5 rounded bg-white border border-amber-200 text-amber-800">${codeOf('cf_activate')} + N°</strong>${cfOff ? ` (annuler : <strong class="font-mono text-amber-800">${codeOf('cf_deactivate')}</strong>)` : ''}.</li>` : ''}
${cfuOn ? `<li>• Renvoi si absent : composez <strong class="font-mono px-1.5 py-0.5 rounded bg-white border border-amber-200 text-amber-800">${codeOf('cfu_activate')} + N°</strong>.</li>` : ''}
${dndTog || dndOn ? `<li>• Mode DND (Ne Pas Déranger) : composez <strong class="font-mono px-1.5 py-0.5 rounded bg-white border border-amber-200 text-amber-800">${dndTog ? codeOf('dnd_toggle') : codeOf('dnd_activate')}</strong>.</li>` : ''}
<li>• Toutes ces options sont synchronisées en direct avec l'onglet <em>Mes Postes</em>.</li>
</ul>
</div>
<div class="ucp-print-avoid-break rounded-2xl border border-emerald-200 bg-emerald-50/70 p-4 space-y-2">
<div class="flex items-center gap-2 text-xs font-bold text-emerald-900 uppercase tracking-wide">
<span>🎧</span>
<span>Interception &amp; Prise d'Appel</span>
</div>
<ul class="text-[11px] text-slate-700 space-y-1.5 leading-relaxed">
${pkBlf ? `<li>• Poste qui clignote (BLF) : appuyez sur sa touche ou composez <strong class="font-mono px-1.5 py-0.5 rounded bg-white border border-emerald-200 text-emerald-800">${codeOf('pickup_blf')} + Poste</strong>.</li>` : ''}
${pkGrp ? `<li>• Intercepter un poste du groupe : composez <strong class="font-mono px-1.5 py-0.5 rounded bg-white border border-emerald-200 text-emerald-800">${codeOf('pickup_group')}</strong>.</li>` : ''}
<li>• En appel : utilisez la touche <strong>Transfert</strong> de votre téléphone + n° du collègue.</li>
</ul>
</div>
`;
// 2. Regroupement des codes actifs par catégorie
const q = (document.getElementById('ucp-guide-search')?.value || '').trim().toLowerCase();
const filtered = allCodes.filter(c => {
if (!q) return true;
return String(c.code || '').toLowerCase().includes(q)
|| String(c.name || '').toLowerCase().includes(q)
|| String(c.description || '').toLowerCase().includes(q)
|| String(c.category || '').toLowerCase().includes(q);
});
if (!filtered.length) {
catBox.innerHTML = `<div class="rounded-2xl border border-slate-200 bg-slate-50 p-6 text-center text-xs text-slate-500">Aucun code de fonction correspondant à « ${escapeHtml(q)} ».</div>`;
return;
}
const catMeta = {
voicemail: { label: 'Messagerie Vocale', icon: '📬', badge: 'bg-indigo-50 text-indigo-700 border-indigo-200', header: 'bg-indigo-50/60 border-indigo-100 text-indigo-950' },
forward: { label: "Renvois d'Appel", icon: '↪️', badge: 'bg-amber-50 text-amber-800 border-amber-200', header: 'bg-amber-50/60 border-amber-100 text-amber-950' },
dnd: { label: 'Ne Pas Déranger (DND)', icon: '🔕', badge: 'bg-rose-50 text-rose-700 border-rose-200', header: 'bg-rose-50/60 border-rose-100 text-rose-950' },
pickup: { label: "Interception d'Appels", icon: '📲', badge: 'bg-emerald-50 text-emerald-700 border-emerald-200', header: 'bg-emerald-50/60 border-emerald-100 text-emerald-950' },
security: { label: 'Liste Noire & Blocage Appels', icon: '🛡️', badge: 'bg-slate-100 text-slate-700 border-slate-300', header: 'bg-slate-100/80 border-slate-200 text-slate-900' },
tools: { label: 'Outils, Tests Audio & Services', icon: '🛠️', badge: 'bg-sky-50 text-sky-700 border-sky-200', header: 'bg-sky-50/60 border-sky-100 text-sky-950' },
};
const orderedCats = ['voicemail', 'forward', 'dnd', 'pickup', 'security', 'tools'];
const grouped = new Map();
filtered.forEach(c => {
const key = c.category || 'tools';
if (!grouped.has(key)) grouped.set(key, []);
grouped.get(key).push(c);
});
const catKeys = [
...orderedCats.filter(k => grouped.has(k)),
...[...grouped.keys()].filter(k => !orderedCats.includes(k)),
];
catBox.innerHTML = catKeys.map(catKey => {
const meta = catMeta[catKey] || { label: catKey, icon: '📌', badge: 'bg-slate-100 text-slate-700 border-slate-300', header: 'bg-slate-50 border-slate-200 text-slate-900' };
const items = grouped.get(catKey) || [];
const rows = items.map(item => `
<div class="ucp-print-row flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 px-4 py-3 border-t border-slate-100">
<div class="w-24 shrink-0">
<span class="inline-flex items-center justify-center min-w-[3.25rem] px-2.5 py-1 rounded-lg border font-mono text-xs font-extrabold tracking-wide ${meta.badge}">
${escapeHtml(item.code)}
</span>
</div>
<div class="min-w-0 flex-1">
<div class="text-xs font-bold text-slate-900">${escapeHtml(item.name)}</div>
<p class="text-[11px] text-slate-600 mt-0.5 leading-snug">${escapeHtml(item.description || '')}</p>
</div>
</div>
`).join('');
return `
<div class="ucp-print-avoid-break rounded-2xl border border-slate-200 overflow-hidden bg-white">
<div class="px-4 py-2.5 border-b border-slate-200 flex items-center justify-between ${meta.header}">
<div class="flex items-center gap-2 text-xs font-extrabold uppercase tracking-wide">
<span>${meta.icon}</span>
<span>${escapeHtml(meta.label)}</span>
</div>
<span class="text-[10px] font-semibold opacity-75">${items.length} code(s)</span>
</div>
<div>${rows}</div>
</div>
`;
}).join('');
}
// -----------------------------------------------------------------------------
// 6. CHANGEMENT DE MOT DE PASSE UCP
// -----------------------------------------------------------------------------
const passwordForm = document.getElementById('ucp-password-form');
+113 -1
View File
@@ -47,6 +47,54 @@ $csrfToken = UcpAuth::getCsrfToken();
::-webkit-scrollbar-track { background: #f1f5f9; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
@page {
size: A4 portrait;
margin: 11mm 12mm;
}
@media print {
html, body {
background: #ffffff !important;
color: #0f172a !important;
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
header, #ucp-toast-container, #ucp-profile-modal, .no-print {
display: none !important;
}
main {
max-width: 100% !important;
padding: 0 !important;
margin: 0 !important;
}
.ucp-panel {
display: none !important;
}
#ucp-panel-guide {
display: block !important;
margin: 0 !important;
padding: 0 !important;
}
#ucp-guide-printable {
border: none !important;
box-shadow: none !important;
padding: 0 !important;
margin: 0 !important;
}
.ucp-print-avoid-break {
break-inside: avoid !important;
page-break-inside: avoid !important;
}
.ucp-print-3col {
display: grid !important;
grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
gap: 0.75rem !important;
}
.ucp-print-row {
flex-direction: row !important;
align-items: center !important;
}
}
</style>
</head>
<body class="min-h-full font-sans antialiased text-slate-800">
@@ -160,6 +208,10 @@ $csrfToken = UcpAuth::getCsrfToken();
class="ucp-tab-btn px-3.5 py-2 rounded-xl text-xs font-semibold transition flex items-center gap-2 text-slate-600 hover:bg-slate-100">
<span>📖 Annuaire &amp; Profil</span>
</button>
<button type="button" data-ucp-tab="guide" onclick="switchUcpTab('guide')"
class="ucp-tab-btn px-3.5 py-2 rounded-xl text-xs font-semibold transition flex items-center gap-2 text-slate-600 hover:bg-slate-100">
<span>📋 Notice &amp; Codes</span>
</button>
<button type="button" data-ucp-tab="account" onclick="switchUcpTab('account')"
class="ucp-tab-btn px-3.5 py-2 rounded-xl text-xs font-semibold transition flex items-center gap-2 text-slate-600 hover:bg-slate-100">
<span>🔐 Mon Compte</span>
@@ -308,7 +360,67 @@ $csrfToken = UcpAuth::getCsrfToken();
</div>
</section>
<!-- ONGLET 5 : MON COMPTE -->
<!-- ONGLET 5 : NOTICE TÉLÉPHONIE, CODES DE FONCTION & EXPORT PDF -->
<section id="ucp-panel-guide" class="ucp-panel hidden space-y-5">
<!-- Barre d'actions écran uniquement -->
<div class="no-print flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<h2 class="text-lg font-bold text-slate-900">Notice d'Utilisation &amp; Codes de Fonction</h2>
<p class="text-xs text-slate-500">Mémo des raccourcis téléphoniques actifs et fiche pratique imprimable en PDF.</p>
</div>
<div class="flex flex-wrap items-center gap-2.5">
<input type="text" id="ucp-guide-search" oninput="renderFeatureGuide()" placeholder="Filtrer un code ou service (*97, renvoi…)"
class="w-full sm:w-64 px-3.5 py-2 rounded-xl bg-white border border-slate-300 text-xs text-slate-900 focus:ring-2 focus:ring-indigo-500/30 focus:border-indigo-500 focus:outline-none">
<button type="button" onclick="printFeatureGuide()"
class="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-semibold shadow-sm shadow-indigo-500/20 transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/></svg>
<span>Imprimer / Exporter en PDF</span>
</button>
</div>
</div>
<!-- Document A4 Propre & Imprimable -->
<div id="ucp-guide-printable" class="ucp-card rounded-3xl p-6 sm:p-8 space-y-6">
<!-- En-tête Corporate du document -->
<div class="ucp-print-avoid-break ucp-print-row rounded-2xl bg-gradient-to-r from-slate-900 via-indigo-950 to-slate-900 text-white p-5 sm:p-6 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div class="flex items-center gap-4 min-w-0">
<div class="h-14 w-14 sm:h-16 sm:w-16 rounded-2xl bg-white p-2 flex items-center justify-center shrink-0 shadow-md">
<?php if ($companyLogo !== ''): ?>
<img src="<?= htmlspecialchars($companyLogo) ?>" alt="Logo" class="max-h-full max-w-full object-contain">
<?php else: ?>
<img src="/assets/logo.png?v=1" alt="NexusPBX" class="max-h-full max-w-full object-contain">
<?php endif; ?>
</div>
<div class="min-w-0">
<div class="text-[10px] font-bold uppercase tracking-widest text-indigo-300">Fiche Mémo Utilisateur • Téléphonie IP</div>
<h3 class="text-lg sm:text-xl font-extrabold tracking-tight truncate"><?= htmlspecialchars($companyName ?: 'NexusPBX') ?></h3>
<p class="text-xs text-slate-300 mt-0.5">Guide des codes de fonction, messagerie vocale et renvois d'appel</p>
</div>
</div>
<div class="sm:text-right bg-white/10 border border-white/15 rounded-xl px-4 py-2.5 text-xs space-y-1 shrink-0">
<div class="text-slate-300">Utilisateur : <strong class="text-white"><?= htmlspecialchars((string)($currentUser['display_name'] ?: $currentUser['username'])) ?></strong></div>
<div class="text-slate-300">Poste(s) : <strong id="ucp-guide-user-exts" class="font-mono text-indigo-200">—</strong></div>
<div class="text-[10px] text-slate-400">Édité le <span id="ucp-guide-print-date"><?= date('d/m/Y') ?></span> • v<?= htmlspecialchars($appVersion) ?></div>
</div>
</div>
<!-- Encarts Notice Pratique Rapide -->
<div id="ucp-guide-quickcards" class="ucp-print-3col grid grid-cols-1 md:grid-cols-3 gap-4"></div>
<!-- Liste complète structurée par catégorie -->
<div id="ucp-guide-categories" class="space-y-4">
<div class="text-center text-sm text-slate-400 py-8">Chargement des codes de fonction…</div>
</div>
<!-- Pied de document -->
<div class="ucp-print-avoid-break pt-4 border-t border-slate-200 flex flex-col sm:flex-row sm:items-center justify-between gap-2 text-[11px] text-slate-500">
<div>💡 <strong>Astuce :</strong> composez ces codes directement sur votre combiné IP, ou pilotez DND / renvois / messagerie depuis le portail UCP.</div>
<div class="font-semibold text-slate-600"><?= htmlspecialchars($companyName ?: 'NexusPBX') ?> — Document interne</div>
</div>
</div>
</section>
<!-- ONGLET 6 : MON COMPTE -->
<section id="ucp-panel-account" class="ucp-panel hidden space-y-5">
<div>
<h2 class="text-lg font-bold text-slate-900">Mon Compte UCP</h2>
+2 -1
View File
@@ -1,9 +1,10 @@
{
"version": "2.5.2",
"version": "2.5.3",
"codename": "Pulsar UCP",
"release_date": "2026-11-11",
"min_php_version": "8.2",
"changelog": [
"Nouveauté v2.5.3 : onglet UCP « Notice & Codes » avec mémo pas-à-pas, codes de fonction dynamiques et export PDF / impression A4 corporate (logo, encarts, poste utilisateur)",
"Correctif v2.5.2 : purge systématique try/finally + auto-nettoyage des dossiers temporaires HA /tmp (élimine la saturation tmpfs 100% qui bloquait la réplication)",
"Correctif v2.5.1 : auto-déploiement automatique Nginx/Fail2ban UCP même après un git pull manuel",
"Correctif v2.5.1 : vérification d'intégrité & restauration SQLite HA via PDO / SQLite3::backup sans dépendance CLI sqlite3 et anti-collision /tmp",