feat(ucp): onglet Notice & Codes de fonction avec fiche PDF imprimable (v2.5.3)
This commit is contained in:
1 parent
2a7bb2a122
commit
d618d426e7
5 files changed
+289
-3
No files matched your search
@@ -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
@@ -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 & 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 & 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
@@ -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 & 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 & 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 & 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>
|
||||
|
||||
Reference in new issue
Block a user