feat(branding): Customer dashboard header toggles in Branding page
Adds back the "Show logo" / "Show company name" toggles for the customer dashboard, scoped to /customer/* surfaces only. Lives as a dedicated card at the bottom of Settings → Branding, gated by the customerPortal feature flag so admins who haven't enabled the portal don't see it. * Backend: restored GET/PUT /admin/settings/customer-surface endpoints, whitelisted only to the two branding keys (customer_show_logo, customer_show_company_name). The calendar/quotes/bills feature globals that used to live on this endpoint are now driven by the Features tab (feature_flags table). * customerAccountsService.getCustomerSurfaceGlobals() reads from app_settings again so /api/customer/auth/session honours the toggles in its branding payload. * New CustomerDashboardBrandingCard component with its own save flow — separate from the main BrandingPage payload so flipping a toggle doesn't replay the full branding mutation. Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
@@ -134,6 +134,88 @@ router.get('/', adminAuth, requirePermission('settings.view'), async (req, res)
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Customer-surface branding settings (#354 follow-up).
|
||||
*
|
||||
* Two toggles control what shows in the customer dashboard header:
|
||||
* customer_show_logo (default true)
|
||||
* customer_show_company_name (default true)
|
||||
*
|
||||
* The Calendar / Quotes / Bills feature globals that used to live here
|
||||
* have moved to the maintainer's Features tab (feature_flags table).
|
||||
*
|
||||
* IMPORTANT: both routes MUST be registered before the generic
|
||||
* `router.get('/:type', ...)` below — Express matches routes in
|
||||
* registration order.
|
||||
*/
|
||||
router.get('/customer-surface', adminAuth, requirePermission('settings.view'), async (req, res) => {
|
||||
try {
|
||||
const rows = await db('app_settings')
|
||||
.where('setting_type', 'customer_surface')
|
||||
.select('setting_key', 'setting_value');
|
||||
|
||||
const settings = {};
|
||||
for (const r of rows) {
|
||||
let value = r.setting_value;
|
||||
if (value === null || value === undefined) {
|
||||
settings[r.setting_key] = null;
|
||||
continue;
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
settings[r.setting_key] = value;
|
||||
} else {
|
||||
try { settings[r.setting_key] = JSON.parse(value); }
|
||||
catch { settings[r.setting_key] = value; }
|
||||
}
|
||||
}
|
||||
|
||||
res.json(settings);
|
||||
} catch (error) {
|
||||
console.error('Customer surface settings fetch error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch customer surface settings' });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/customer-surface', adminAuth, requirePermission('settings.edit'), async (req, res) => {
|
||||
try {
|
||||
// Branding-only whitelist (calendar/quotes/bills feature globals
|
||||
// moved to the Features tab / feature_flags table).
|
||||
const allowed = [
|
||||
'customer_show_logo',
|
||||
'customer_show_company_name',
|
||||
];
|
||||
const updates = [];
|
||||
for (const key of allowed) {
|
||||
if (Object.prototype.hasOwnProperty.call(req.body, key)) {
|
||||
const value = !!req.body[key];
|
||||
updates.push({ setting_key: key, setting_value: JSON.stringify(value), setting_type: 'customer_surface' });
|
||||
}
|
||||
}
|
||||
|
||||
for (const u of updates) {
|
||||
const existing = await db('app_settings').where('setting_key', u.setting_key).first();
|
||||
if (existing) {
|
||||
await db('app_settings').where('setting_key', u.setting_key).update({
|
||||
setting_value: u.setting_value,
|
||||
setting_type: u.setting_type,
|
||||
updated_at: new Date(),
|
||||
});
|
||||
} else {
|
||||
await db('app_settings').insert({ ...u, created_at: new Date(), updated_at: new Date() });
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the public-site cache so any consumer relying on it
|
||||
// (e.g. customer login footer if it picks these up) refetches.
|
||||
clearPublicSiteCache();
|
||||
|
||||
res.json({ message: 'Customer surface settings updated', updated: updates.map((u) => u.setting_key) });
|
||||
} catch (error) {
|
||||
console.error('Customer surface settings save error:', error);
|
||||
res.status(500).json({ error: 'Failed to save customer surface settings' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get settings by type
|
||||
router.get('/:type', adminAuth, requirePermission('settings.view'), async (req, res) => {
|
||||
try {
|
||||
|
||||
@@ -761,23 +761,36 @@ async function isCustomerPortalEnabled() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer-surface global feature toggles. The customer-portal feature
|
||||
* flag (read above) is the master switch; calendar / quotes / bills are
|
||||
* locked behind their own maintainer-side flags (Settings → Features)
|
||||
* but those surfaces aren't yet built — return false so the customer
|
||||
* dashboard doesn't render placeholder tabs.
|
||||
* Customer-surface global toggles. Branding visibility (logo /
|
||||
* company name in the customer dashboard header) lives in
|
||||
* app_settings under setting_type='customer_surface' and is edited
|
||||
* from the Branding page (Customer dashboard card, gated by the
|
||||
* customerPortal feature flag).
|
||||
*
|
||||
* Branding visibility (logo / company name) is no longer per-instance
|
||||
* configurable on the customer surface — the customer layout always
|
||||
* shows the configured brand to keep parity with /admin.
|
||||
* Calendar / Quotes / Bills feature globals are intentionally OFF
|
||||
* here — those surfaces are now governed by the maintainer's
|
||||
* feature_flags table (Settings → Features), not by app_settings.
|
||||
*
|
||||
* Returns sane defaults when the keys aren't present so an install
|
||||
* missing migration 092 doesn't crash — branding defaults ON to
|
||||
* match the visual state before the toggle existed.
|
||||
*/
|
||||
async function getCustomerSurfaceGlobals() {
|
||||
const rows = await db('app_settings').where('setting_type', 'customer_surface').select('setting_key', 'setting_value');
|
||||
const map = {};
|
||||
for (const r of rows) {
|
||||
let v = r.setting_value;
|
||||
if (typeof v === 'string') {
|
||||
try { v = JSON.parse(v); } catch { /* leave as-is */ }
|
||||
}
|
||||
map[r.setting_key] = v;
|
||||
}
|
||||
return {
|
||||
calendarEnabled: false,
|
||||
quotesEnabled: false,
|
||||
billsEnabled: false,
|
||||
showLogo: true,
|
||||
showCompanyName: true,
|
||||
showLogo: map.customer_show_logo !== false, // default true
|
||||
showCompanyName: map.customer_show_company_name !== false, // default true
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user