183 lines
7.3 KiB
JavaScript
183 lines
7.3 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'
|
|
import { withBase } from '@/utils/basePath'
|
|
|
|
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 withBase(await 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 withBase((value === undefined || value === null) ? '/ge-monogram.svg' : value)
|
|
}
|
|
|
|
// Logo printed on machine inspection badges.
|
|
export async function getBadgeLogo() {
|
|
return withBase(await getSetting('badge_logo', '/ge-aerospace-logo.svg'))
|
|
}
|
|
|
|
// Browser-tab favicon. Empty = keep the shipped /favicon.svg.
|
|
export async function getFavicon() {
|
|
const value = await getSetting('site_favicon', '')
|
|
return value ? withBase(value) : value
|
|
}
|
|
|
|
// 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).
|
|
//
|
|
// The defaults are BLANK and the integration ships OFF, matching the server.
|
|
// They used to be one organisation's QA ServiceNow tenant, so any site that had
|
|
// not configured ServiceNow still rendered ticket links - pointing its
|
|
// technicians at a tenant they cannot reach and, for an adopting site, at
|
|
// somebody else's. A blank URL renders tickets as plain text, which is the
|
|
// correct behaviour for "not configured".
|
|
export async function getServicenowUrls() {
|
|
const enabledRaw = await getSetting('servicenow_enabled', 'false')
|
|
const enabled = enabledRaw !== 'false' && enabledRaw !== '0' && enabledRaw !== false
|
|
const incidentUrl = await getSetting('servicenow_incident_url', '')
|
|
const changeUrl = await getSetting('servicenow_change_url', '')
|
|
const searchUrl = await getSetting('servicenow_search_url', '')
|
|
return { enabled, incidentUrl, changeUrl, searchUrl }
|
|
}
|
|
|
|
// Deep links into the Azure portal for a device ShopDB knows the Entra ids of.
|
|
//
|
|
// Both are TEMPLATES held in settings rather than built here, because the host
|
|
// is a tenant fact: this site is on the US Government cloud (portal.azure.us)
|
|
// and the commercial cloud is portal.azure.com. Microsoft also moves these
|
|
// blade paths from time to time, and a site can fix that itself by editing a
|
|
// setting instead of waiting for a release.
|
|
//
|
|
// Empty is the default and means "no link" - the id still renders as text.
|
|
export async function getEntraDeviceUrls() {
|
|
const entraUrl = await getSetting('entra_device_url', '')
|
|
const intuneUrl = await getSetting('intune_device_url', '')
|
|
return { entraUrl, intuneUrl }
|
|
}
|
|
|
|
// 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') // ADR-015-OK: GE Aerospace-wide domain, and only the DEFAULT of a documented setting every site can override.
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|