diff --git a/src/UcpManager.php b/src/UcpManager.php
index 2927432..6230835 100644
--- a/src/UcpManager.php
+++ b/src/UcpManager.php
@@ -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
// =========================================================================
diff --git a/ucp/api.php b/ucp/api.php
index f8b30e6..d21d692 100644
--- a/ucp/api.php
+++ b/ucp/api.php
@@ -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;
diff --git a/ucp/assets/ucp.js b/ucp/assets/ucp.js
index 2377833..df9b61a 100644
--- a/ucp/assets/ucp.js
+++ b/ucp/assets/ucp.js
@@ -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 = `
+
+
+ 📬
+ Utiliser la Messagerie Vocale
+
+
+ ${vmOwn ? `- • Depuis votre poste : composez ${codeOf('voicemail_own')} (accès direct sans PIN).
` : ''}
+ ${vmOther ? `- • Depuis un autre poste : composez ${codeOf('voicemail_general')} + n° de poste + code PIN.
` : ''}
+ - • Menu vocal : 1 Écouter • 2 Changer de dossier • 3 Options / Supprimer.
+ - • Depuis l'UCP : écoutez, téléchargez (.wav) et changez votre PIN sans appeler.
+
+
+
+
+
+ ↪️
+ Gérer Renvois & Ne Pas Déranger
+
+
+ ${cfOn ? `- • Renvoi immédiat : composez ${codeOf('cf_activate')} + N°${cfOff ? ` (annuler : ${codeOf('cf_deactivate')})` : ''}.
` : ''}
+ ${cfuOn ? `- • Renvoi si absent : composez ${codeOf('cfu_activate')} + N°.
` : ''}
+ ${dndTog || dndOn ? `- • Mode DND (Ne Pas Déranger) : composez ${dndTog ? codeOf('dnd_toggle') : codeOf('dnd_activate')}.
` : ''}
+ - • Toutes ces options sont synchronisées en direct avec l'onglet Mes Postes.
+
+
+
+
+
+ 🎧
+ Interception & Prise d'Appel
+
+
+ ${pkBlf ? `- • Poste qui clignote (BLF) : appuyez sur sa touche ou composez ${codeOf('pickup_blf')} + Poste.
` : ''}
+ ${pkGrp ? `- • Intercepter un poste du groupe : composez ${codeOf('pickup_group')}.
` : ''}
+ - • En appel : utilisez la touche Transfert de votre téléphone + n° du collègue.
+
+
+ `;
+
+ // 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 = `Aucun code de fonction correspondant à « ${escapeHtml(q)} ».
`;
+ 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 => `
+
+
+
+
+
+
${escapeHtml(item.name)}
+
${escapeHtml(item.description || '')}
+
+
+ `).join('');
+
+ return `
+
+ `;
+ }).join('');
+}
+
+// -----------------------------------------------------------------------------
+// 6. CHANGEMENT DE MOT DE PASSE UCP
// -----------------------------------------------------------------------------
const passwordForm = document.getElementById('ucp-password-form');
diff --git a/ucp/index.php b/ucp/index.php
index def8e1a..7a67c1b 100644
--- a/ucp/index.php
+++ b/ucp/index.php
@@ -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;
+ }
+ }
@@ -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">
📖 Annuaire & Profil
+