Files
shopdb-flask/frontend/src/utils/siteSettings.js
cproudlock 7d309aabeb
All checks were successful
CI / backend (push) Successful in 1m21s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 8s
Final-pass polish: support contact UX, audit tooltip, doc refresh
Support teams: contact management moved from a row expander to a modal
(Contacts (N) button per team); application detail Support card and the
modal show Email (mailto) and Teams chat buttons for contacts with an
SSO, derived as sso@ + a new contact_email_domain site setting
(default geaerospace.com, blank hides the buttons).

Audit log: hovering a user SSO shows the full name, resolved
best-effort from the employee directory in either mode.

Docs/hygiene from a standards review: CLAUDE.md active-state,
CONTRACT-STABILITY.md and README brought to contract 0.10.0 / 11
plugins / migration head 7d22; get_asset_panels endpoint path fixed in
the hook docstring; leftover debug console.logs removed.

781 tests pass; contacts modal, action-button hrefs, and the audit
tooltip verified live.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 11:22:01 -04:00

168 lines
6.5 KiB
JavaScript

// Shared read-through for public site settings (site_base_url, facility_name).
// The settings GET is public (jwt optional), so the kiosk dashboard and the
// print views can read these without auth. Fetched once and cached per page.
import { settingsApi } from '@/api'
let settingsCache = null
async function loadSettings() {
if (settingsCache) return settingsCache
try {
const response = await settingsApi.list()
const items = response.data?.data || response.data || []
settingsCache = {}
for (const s of items) settingsCache[s.key] = s.value
} catch (err) {
console.error('Error loading site settings:', err)
settingsCache = {}
}
return settingsCache
}
export async function getSetting(key, fallback = '') {
const settings = await loadSettings()
const value = settings[key]
return (value === undefined || value === null || value === '') ? fallback : value
}
// Public base URL for QR codes / absolute links. Falls back to the current
// browsing origin when a site has not set one.
export async function getSiteBaseUrl() {
return getSetting('site_base_url', window.location.origin)
}
// Facility name shown on the shopfloor dashboard.
export async function getFacilityName() {
return getSetting('facility_name', 'ShopDB')
}
// Main site logo (sidebar, login, dashboard header). Fallback = shipped GE mark.
export async function getSiteLogo() {
return getSetting('site_logo', '/ge-aerospace-logo.svg')
}
// Logo composited into the center of printer QR codes. Empty = no overlay.
// Note: '' is a valid "no overlay" value, so read the raw setting rather than
// getSetting (which swaps '' for the fallback).
export async function getQrLogo() {
const settings = await loadSettings()
const value = settings['qr_logo']
return (value === undefined || value === null) ? '/ge-monogram.svg' : value
}
// Logo printed on machine inspection badges.
export async function getBadgeLogo() {
return getSetting('badge_logo', '/ge-aerospace-logo.svg')
}
// Browser-tab favicon. Empty = keep the shipped /favicon.svg.
export async function getFavicon() {
return getSetting('site_favicon', '')
}
// Brand primary color override. Empty = built-in palette from style.css.
export async function getBrandPrimaryColor() {
return getSetting('brand_primary_color', '')
}
// Primary hover/active color (--primary-dark). Empty = derive from primary.
export async function getBrandPrimaryDarkColor() {
return getSetting('brand_primary_dark_color', '')
}
// Accent color (--secondary). Empty = built-in palette from style.css.
export async function getBrandAccentColor() {
return getSetting('brand_accent_color', '')
}
// Sidebar background color (--sidebar-bg). Empty = built-in palette.
export async function getBrandSidebarColor() {
return getSetting('brand_sidebar_color', '')
}
// Darken a #rrggbb hex color by the given fraction (0.15 = 15% darker).
// Returns null for anything that is not a 6-digit hex so callers can skip it.
function darkenHexColor(hex, fraction) {
const match = /^#([0-9a-fA-F]{6})$/.exec(hex)
if (!match) return null
const num = parseInt(match[1], 16)
const scale = 1 - fraction
const red = Math.round(((num >> 16) & 0xff) * scale)
const green = Math.round(((num >> 8) & 0xff) * scale)
const blue = Math.round((num & 0xff) * scale)
const toHex = (channel) => channel.toString(16).padStart(2, '0')
return `#${toHex(red)}${toHex(green)}${toHex(blue)}`
}
// ServiceNow ticket-link config. Returns the enabled flag, incident/change URL
// templates, and the global-search URL template (all with a {ticket}
// placeholder). Defaults mirror the previously-hardcoded GE ServiceNow URLs.
export async function getServicenowUrls() {
const enabledRaw = await getSetting('servicenow_enabled', 'true')
const enabled = enabledRaw !== 'false' && enabledRaw !== '0' && enabledRaw !== false
const incidentUrl = await getSetting(
'servicenow_incident_url',
'https://geaerospaceqa.service-now.com/now/nav/ui/search/0f8b85d0c7922010099a308dc7c2606a/params/search-term/{ticket}/global-search-data-config-id/c861cea2c7022010099a308dc7c26041/'
)
const changeUrl = await getSetting(
'servicenow_change_url',
'https://geaerospaceqa.service-now.com/now/nav/ui/search/0f8b85d0c7922010099a308dc7c2606a/params/search-term/{ticket}/global-search-data-config-id/c861cea2c7022010099a308dc7c26041/'
)
const searchUrl = await getSetting(
'servicenow_search_url',
'https://geaerospaceqa.service-now.com/now/nav/ui/search/0f8b85d0c7922010099a308dc7c2606a/params/search-term/{ticket}/global-search-data-config-id/c861cea2c7022010099a308dc7c26041/'
)
return { enabled, incidentUrl, changeUrl, searchUrl }
}
// Printer hostname template. {ip} is replaced with the dash-separated IP.
export async function getPrinterHostnameTemplate() {
return getSetting('printer_hostname_template', 'Printer-{ip}.printer.geaerospace.net')
}
// Email domain appended to a contact SSO to build email/Teams links
// (sso@domain). Unset falls back to geaerospace.com; an explicit blank value
// means "disable contact action buttons", so read the raw setting rather than
// getSetting (which swaps '' for the fallback).
export async function getContactEmailDomain() {
const settings = await loadSettings()
const value = settings['contact_email_domain']
return (value === undefined || value === null) ? 'geaerospace.com' : value
}
// Apply per-site favicon + brand color at bootstrap. Empty settings keep the
// shipped defaults. Do not touch the style.css palette here.
export async function applyBranding() {
const favicon = await getFavicon()
if (favicon) {
let link = document.querySelector('link[rel="icon"]')
if (!link) {
link = document.createElement('link')
link.rel = 'icon'
document.head.appendChild(link)
}
link.href = favicon
}
const rootStyle = document.documentElement.style
const primaryColor = await getBrandPrimaryColor()
if (primaryColor) {
rootStyle.setProperty('--primary', primaryColor)
}
// Hover color: use the explicit value, else darken primary ~15%.
const primaryDarkColor = await getBrandPrimaryDarkColor()
if (primaryDarkColor) {
rootStyle.setProperty('--primary-dark', primaryDarkColor)
} else if (primaryColor) {
const derived = darkenHexColor(primaryColor, 0.15)
if (derived) rootStyle.setProperty('--primary-dark', derived)
}
const accentColor = await getBrandAccentColor()
if (accentColor) {
rootStyle.setProperty('--secondary', accentColor)
}
const sidebarColor = await getBrandSidebarColor()
if (sidebarColor) {
rootStyle.setProperty('--sidebar-bg', sidebarColor)
}
}