feat(branding): v2.7.0 logo specifique optionnel pour UCP avec repli sur logo principal ou NexusPBX

This commit is contained in:
sorcier committed 2026-11-12 11:35:09 +01:00
1 parent ddbaabb657
commit ba04af32bb
7 files changed
+130 -32

No files matched your search

+40 -17
View File
@@ -1802,30 +1802,53 @@ try {
case 'save_company_branding': case 'save_company_branding':
$company_name = trim((string)($payload['company_name'] ?? '')); $company_name = trim((string)($payload['company_name'] ?? ''));
$company_logo = trim((string)($payload['company_logo'] ?? '')); $company_logo = trim((string)($payload['company_logo'] ?? ''));
$ucp_logo = trim((string)($payload['ucp_logo'] ?? ''));
if ($company_logo !== '' && !preg_match('/^data:image\/(png|jpeg|jpg|webp|svg\+xml|gif);base64,/i', $company_logo)) { foreach (['company_logo' => $company_logo, 'ucp_logo' => $ucp_logo] as $fieldKey => $logoVal) {
throw new InvalidArgumentException("Format d'image invalide (utilisez PNG, SVG, JPG ou WebP)."); if (!array_key_exists($fieldKey, $payload)) {
} continue;
if (strlen($company_logo) > 2 * 1024 * 1024) { }
throw new InvalidArgumentException("Le logo est trop volumineux (max 1.5 Mo)."); if ($logoVal !== '' && !preg_match('/^data:image\/(png|jpeg|jpg|webp|svg\+xml|gif);base64,/i', $logoVal)) {
throw new InvalidArgumentException("Format d'image invalide (utilisez PNG, SVG, JPG ou WebP).");
}
if (strlen($logoVal) > 2 * 1024 * 1024) {
throw new InvalidArgumentException("Le logo est trop volumineux (max 1.5 Mo).");
}
} }
if (array_key_exists('company_name', $payload) && array_key_exists('company_logo', $payload)) { $sets = [];
$stmt = $db->prepare('UPDATE settings SET company_name = :cn, company_logo = :cl, updated_at = CURRENT_TIMESTAMP WHERE id = 1'); $params = [];
$stmt->execute([':cn' => $company_name, ':cl' => $company_logo]); if (array_key_exists('company_name', $payload)) {
} elseif (array_key_exists('company_logo', $payload)) { $sets[] = 'company_name = :cn';
$stmt = $db->prepare('UPDATE settings SET company_logo = :cl, updated_at = CURRENT_TIMESTAMP WHERE id = 1'); $params[':cn'] = $company_name;
$stmt->execute([':cl' => $company_logo]); }
} elseif (array_key_exists('company_name', $payload)) { if (array_key_exists('company_logo', $payload)) {
$stmt = $db->prepare('UPDATE settings SET company_name = :cn, updated_at = CURRENT_TIMESTAMP WHERE id = 1'); $sets[] = 'company_logo = :cl';
$stmt->execute([':cn' => $company_name]); $params[':cl'] = $company_logo;
}
if (array_key_exists('ucp_logo', $payload)) {
$sets[] = 'ucp_logo = :ul';
$params[':ul'] = $ucp_logo;
}
if ($sets) {
$sets[] = 'updated_at = CURRENT_TIMESTAMP';
$stmt = $db->prepare('UPDATE settings SET ' . implode(', ', $sets) . ' WHERE id = 1');
$stmt->execute($params);
}
$msg = 'Logo et identité entreprise mis à jour sur NexusPBX !';
if (array_key_exists('ucp_logo', $payload) && !array_key_exists('company_logo', $payload) && !array_key_exists('company_name', $payload)) {
$msg = $ucp_logo === ''
? 'Logo UCP réinitialisé (hérite du logo principal).'
: 'Logo spécifique UCP mis à jour !';
} elseif (array_key_exists('company_logo', $payload) && $company_logo === '' && !array_key_exists('company_name', $payload) && !array_key_exists('ucp_logo', $payload)) {
$msg = 'Logo entreprise réinitialisé.';
} }
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
'message' => $company_logo === '' && !array_key_exists('company_name', $payload) 'message' => $msg,
? 'Logo entreprise réinitialisé.'
: 'Logo et identité entreprise mis à jour sur NexusPBX !',
]); ]);
break; break;
+40 -4
View File
@@ -243,6 +243,7 @@ function renderAll() {
function renderCompanyBranding() { function renderCompanyBranding() {
const s = appState.settings || {}; const s = appState.settings || {};
const logo = (s.company_logo || '').trim(); const logo = (s.company_logo || '').trim();
const ucpLogo = (s.ucp_logo || '').trim();
const name = (s.company_name || '').trim(); const name = (s.company_name || '').trim();
const emptyBox = document.getElementById('sidebar-company-empty'); const emptyBox = document.getElementById('sidebar-company-empty');
@@ -270,6 +271,21 @@ function renderCompanyBranding() {
} }
} }
const ucpPreviewImg = document.getElementById('settings-ucp-preview-img');
const ucpPreviewEmpty = document.getElementById('settings-ucp-preview-empty');
if (ucpPreviewImg && ucpPreviewEmpty) {
if (ucpLogo) {
ucpPreviewImg.src = ucpLogo;
ucpPreviewImg.classList.remove('hidden');
ucpPreviewEmpty.classList.add('hidden');
} else {
ucpPreviewImg.src = '';
ucpPreviewImg.classList.add('hidden');
ucpPreviewEmpty.textContent = logo ? 'Hérite du logo principal' : 'NexusPBX par défaut';
ucpPreviewEmpty.classList.remove('hidden');
}
}
if (logo || name) { if (logo || name) {
emptyBox?.classList.add('hidden'); emptyBox?.classList.add('hidden');
filledBox?.classList.remove('hidden'); filledBox?.classList.remove('hidden');
@@ -321,7 +337,11 @@ function triggerCompanyLogoPicker() {
document.getElementById('company-logo-file-input')?.click(); document.getElementById('company-logo-file-input')?.click();
} }
function handleCompanyLogoUpload(input) { function triggerUcpLogoPicker() {
document.getElementById('ucp-logo-file-input')?.click();
}
function uploadBrandingLogo(input, fieldName) {
const file = input?.files?.[0]; const file = input?.files?.[0];
if (!file) return; if (!file) return;
@@ -335,7 +355,7 @@ function handleCompanyLogoUpload(input) {
reader.onload = async (ev) => { reader.onload = async (ev) => {
const dataUrl = String(ev.target?.result || ''); const dataUrl = String(ev.target?.result || '');
try { try {
const res = await apiCall('save_company_branding', { company_logo: dataUrl }); const res = await apiCall('save_company_branding', { [fieldName]: dataUrl });
showToast(res.message, 'success'); showToast(res.message, 'success');
await loadState(); await loadState();
} catch (err) { } catch (err) {
@@ -347,9 +367,17 @@ function handleCompanyLogoUpload(input) {
reader.readAsDataURL(file); reader.readAsDataURL(file);
} }
async function clearCompanyLogo() { function handleCompanyLogoUpload(input) {
uploadBrandingLogo(input, 'company_logo');
}
function handleUcpLogoUpload(input) {
uploadBrandingLogo(input, 'ucp_logo');
}
async function clearBrandingLogo(fieldName) {
try { try {
const res = await apiCall('save_company_branding', { company_logo: '' }); const res = await apiCall('save_company_branding', { [fieldName]: '' });
showToast(res.message, 'info'); showToast(res.message, 'info');
await loadState(); await loadState();
} catch (err) { } catch (err) {
@@ -357,6 +385,14 @@ async function clearCompanyLogo() {
} }
} }
function clearCompanyLogo() {
clearBrandingLogo('company_logo');
}
function clearUcpLogo() {
clearBrandingLogo('ucp_logo');
}
function renderSummaryCards() { function renderSummaryCards() {
const exts = appState.extensions || []; const exts = appState.extensions || [];
const grps = appState.groups || []; const grps = appState.groups || [];
+23
View File
@@ -1648,6 +1648,7 @@ $isDevEnv = file_exists(__DIR__ . '/../.dev')
<form id="settings-form" class="space-y-4"> <form id="settings-form" class="space-y-4">
<!-- Identité & Logo de l'Exploitant --> <!-- Identité & Logo de l'Exploitant -->
<input type="file" id="company-logo-file-input" accept="image/png,image/svg+xml,image/jpeg,image/webp,image/gif" class="hidden" onchange="handleCompanyLogoUpload(this)"> <input type="file" id="company-logo-file-input" accept="image/png,image/svg+xml,image/jpeg,image/webp,image/gif" class="hidden" onchange="handleCompanyLogoUpload(this)">
<input type="file" id="ucp-logo-file-input" accept="image/png,image/svg+xml,image/jpeg,image/webp,image/gif" class="hidden" onchange="handleUcpLogoUpload(this)">
<div class="p-4 rounded-xl bg-anthracite-950 border border-anthracite-700 space-y-3.5"> <div class="p-4 rounded-xl bg-anthracite-950 border border-anthracite-700 space-y-3.5">
<div class="flex items-center justify-between pb-2 border-b border-anthracite-700/60"> <div class="flex items-center justify-between pb-2 border-b border-anthracite-700/60">
<div> <div>
@@ -1675,6 +1676,28 @@ $isDevEnv = file_exists(__DIR__ . '/../.dev')
class="w-full px-3 py-2 rounded-lg bg-anthracite-900 border border-anthracite-700 text-sm text-white"> class="w-full px-3 py-2 rounded-lg bg-anthracite-900 border border-anthracite-700 text-sm text-white">
</div> </div>
</div> </div>
<!-- Logo spécifique UCP (optionnel, fond clair) -->
<div class="pt-3 border-t border-anthracite-800 flex flex-wrap items-center justify-between gap-3">
<div class="flex items-center gap-3 min-w-0">
<div id="settings-ucp-preview-slot" class="w-32 h-12 rounded-lg bg-white/95 border border-dashed border-slate-300 flex items-center justify-center p-1.5 shrink-0">
<img id="settings-ucp-preview-img" src="" alt="Aperçu Logo UCP" class="max-h-9 max-w-full object-contain hidden">
<span id="settings-ucp-preview-empty" class="text-[10px] text-slate-500 italic text-center leading-tight">Hérite du logo principal</span>
</div>
<div class="min-w-0">
<div class="text-xs font-semibold text-slate-200">Logo UCP (Portail Utilisateur — optionnel)</div>
<p class="text-[11px] text-slate-400">Si vide, l'UCP réutilise le logo principal (ou NexusPBX). Utile pour importer un logo foncé adapté au thème clair UCP.</p>
</div>
</div>
<div class="flex items-center gap-2 shrink-0">
<button type="button" onclick="triggerUcpLogoPicker()" class="px-3 py-1.5 rounded-lg bg-slate-700 hover:bg-slate-600 text-xs text-white font-semibold transition">
Importer Logo UCP
</button>
<button type="button" onclick="clearUcpLogo()" class="px-2.5 py-1.5 rounded-lg bg-anthracite-800 hover:bg-red-500/20 border border-anthracite-700 text-xs text-slate-300 hover:text-red-300 transition">
Effacer
</button>
</div>
</div>
</div> </div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
+3
View File
@@ -395,6 +395,9 @@ class Database
if (!in_array('company_logo', $existing, true)) { if (!in_array('company_logo', $existing, true)) {
$db->exec("ALTER TABLE settings ADD COLUMN company_logo TEXT NOT NULL DEFAULT ''"); $db->exec("ALTER TABLE settings ADD COLUMN company_logo TEXT NOT NULL DEFAULT ''");
} }
if (!in_array('ucp_logo', $existing, true)) {
$db->exec("ALTER TABLE settings ADD COLUMN ucp_logo TEXT NOT NULL DEFAULT ''");
}
if (!in_array('default_moh_sound_id', $existing, true)) { if (!in_array('default_moh_sound_id', $existing, true)) {
$db->exec("ALTER TABLE settings ADD COLUMN default_moh_sound_id INTEGER DEFAULT 2"); $db->exec("ALTER TABLE settings ADD COLUMN default_moh_sound_id INTEGER DEFAULT 2");
} }
+7 -3
View File
@@ -98,7 +98,9 @@ try {
switch ($action) { switch ($action) {
case 'get_state': case 'get_state':
$branding = $db->query('SELECT company_name, company_logo FROM settings WHERE id = 1 LIMIT 1')->fetch(PDO::FETCH_ASSOC) ?: []; $branding = $db->query('SELECT company_name, company_logo, ucp_logo FROM settings WHERE id = 1 LIMIT 1')->fetch(PDO::FETCH_ASSOC) ?: [];
$compLogo = trim((string)($branding['company_logo'] ?? ''));
$ucpLogo = trim((string)($branding['ucp_logo'] ?? ''));
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
'data' => [ 'data' => [
@@ -110,8 +112,10 @@ try {
'last_login_at' => $currentUser['last_login_at'], 'last_login_at' => $currentUser['last_login_at'],
], ],
'branding' => [ 'branding' => [
'company_name' => (string)($branding['company_name'] ?? ''), 'company_name' => (string)($branding['company_name'] ?? ''),
'company_logo' => (string)($branding['company_logo'] ?? ''), 'company_logo' => $compLogo,
'ucp_logo' => $ucpLogo,
'effective_logo' => $ucpLogo !== '' ? $ucpLogo : $compLogo,
], ],
'csrf_token' => UcpAuth::getCsrfToken(), 'csrf_token' => UcpAuth::getCsrfToken(),
'extensions' => UcpManager::getExtensionsState($db, $allowedExts, $ucpUserId), 'extensions' => UcpManager::getExtensionsState($db, $allowedExts, $ucpUserId),
+9 -7
View File
@@ -13,9 +13,11 @@ $ucpEnabled = UcpAuth::isUcpEnabled($db);
$currentUser = $ucpEnabled ? UcpAuth::getCurrentUser($db) : null; $currentUser = $ucpEnabled ? UcpAuth::getCurrentUser($db) : null;
$isAuth = ($currentUser !== null); $isAuth = ($currentUser !== null);
$branding = $db->query('SELECT company_name, company_logo FROM settings WHERE id = 1 LIMIT 1')->fetch(\PDO::FETCH_ASSOC) ?: []; $branding = $db->query('SELECT company_name, company_logo, ucp_logo FROM settings WHERE id = 1 LIMIT 1')->fetch(\PDO::FETCH_ASSOC) ?: [];
$companyName = trim((string)($branding['company_name'] ?? '')); $companyName = trim((string)($branding['company_name'] ?? ''));
$companyLogo = trim((string)($branding['company_logo'] ?? '')); $companyLogo = trim((string)($branding['company_logo'] ?? ''));
$ucpLogo = trim((string)($branding['ucp_logo'] ?? ''));
$ucpEffectiveLogo = $ucpLogo !== '' ? $ucpLogo : $companyLogo;
$appVerData = file_exists(__DIR__ . '/../version.json') ? json_decode((string)file_get_contents(__DIR__ . '/../version.json'), true) : []; $appVerData = file_exists(__DIR__ . '/../version.json') ? json_decode((string)file_get_contents(__DIR__ . '/../version.json'), true) : [];
$appVersion = $appVerData['version'] ?? '2.5.0'; $appVersion = $appVerData['version'] ?? '2.5.0';
$csrfToken = UcpAuth::getCsrfToken(); $csrfToken = UcpAuth::getCsrfToken();
@@ -136,8 +138,8 @@ $csrfToken = UcpAuth::getCsrfToken();
<div class="absolute inset-0 bg-white/10"></div> <div class="absolute inset-0 bg-white/10"></div>
<div class="relative z-10 w-full max-w-md bg-white/85 backdrop-blur-xl rounded-3xl p-8 shadow-xl shadow-indigo-900/10 border border-white/80"> <div class="relative z-10 w-full max-w-md bg-white/85 backdrop-blur-xl rounded-3xl p-8 shadow-xl shadow-indigo-900/10 border border-white/80">
<div class="flex flex-col items-center text-center mb-7"> <div class="flex flex-col items-center text-center mb-7">
<?php if ($companyLogo !== ''): ?> <?php if ($ucpEffectiveLogo !== ''): ?>
<img src="<?= htmlspecialchars($companyLogo) ?>" alt="Logo" class="h-14 w-auto object-contain mb-3 rounded-xl"> <img src="<?= htmlspecialchars($ucpEffectiveLogo) ?>" alt="Logo" class="h-14 w-auto object-contain mb-3 rounded-xl">
<?php else: ?> <?php else: ?>
<img src="/assets/logo.png?v=1" alt="NexusPBX" class="h-14 w-auto object-contain mb-3"> <img src="/assets/logo.png?v=1" alt="NexusPBX" class="h-14 w-auto object-contain mb-3">
<?php endif; ?> <?php endif; ?>
@@ -180,8 +182,8 @@ $csrfToken = UcpAuth::getCsrfToken();
<header class="sticky top-0 z-30 bg-white/95 backdrop-blur border-b border-slate-200"> <header class="sticky top-0 z-30 bg-white/95 backdrop-blur border-b border-slate-200">
<div class="max-w-6xl mx-auto px-4 sm:px-6 h-16 flex items-center justify-between gap-4"> <div class="max-w-6xl mx-auto px-4 sm:px-6 h-16 flex items-center justify-between gap-4">
<div class="flex items-center gap-3 min-w-0"> <div class="flex items-center gap-3 min-w-0">
<?php if ($companyLogo !== ''): ?> <?php if ($ucpEffectiveLogo !== ''): ?>
<img src="<?= htmlspecialchars($companyLogo) ?>" alt="Logo" class="h-9 w-auto rounded-lg object-contain shrink-0"> <img src="<?= htmlspecialchars($ucpEffectiveLogo) ?>" alt="Logo" class="h-9 w-auto rounded-lg object-contain shrink-0">
<?php else: ?> <?php else: ?>
<img src="/assets/logo.png?v=1" alt="NexusPBX" class="h-9 w-auto object-contain shrink-0"> <img src="/assets/logo.png?v=1" alt="NexusPBX" class="h-9 w-auto object-contain shrink-0">
<?php endif; ?> <?php endif; ?>
@@ -430,8 +432,8 @@ $csrfToken = UcpAuth::getCsrfToken();
<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="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="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"> <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 !== ''): ?> <?php if ($ucpEffectiveLogo !== ''): ?>
<img src="<?= htmlspecialchars($companyLogo) ?>" alt="Logo" class="max-h-full max-w-full object-contain"> <img src="<?= htmlspecialchars($ucpEffectiveLogo) ?>" alt="Logo" class="max-h-full max-w-full object-contain">
<?php else: ?> <?php else: ?>
<img src="/assets/logo.png?v=1" alt="NexusPBX" class="max-h-full max-w-full object-contain"> <img src="/assets/logo.png?v=1" alt="NexusPBX" class="max-h-full max-w-full object-contain">
<?php endif; ?> <?php endif; ?>
+8 -1
View File
@@ -1,9 +1,16 @@
{ {
"version": "2.6.7", "version": "2.7.0",
"codename": "Pulsar UCP", "codename": "Pulsar UCP",
"release_date": "2026-11-12", "release_date": "2026-11-12",
"min_php_version": "8.2", "min_php_version": "8.2",
"changelog": [ "changelog": [
{
"version": "2.7.0",
"date": "2026-11-12",
"changes": [
"Branding UCP : Ajout d'un logo spécifique optionnel pour le portail UCP (thème clair), avec repli automatique sur le logo principal puis sur le logo NexusPBX par défaut"
]
},
{ {
"version": "2.6.7", "version": "2.6.7",
"date": "2026-11-12", "date": "2026-11-12",