dashboard: resolve employee names from directory/user, GE monogram photo fallback, kiosk sweep + label

- notifications shopfloor feed: resolve the employee name live when the stored
  value is a bare SSO (WJ notifications imported as SSOs, never converted), for
  both single and split-per-employee cards
- employee name resolver: after a directory miss, fall back to the shopdb User
  account (firstname/lastname, keyed by SSO username) so users from other
  locations still show a name
- shopfloor dashboard: employee photo falls back to the GE monogram (own asset,
  independent of the site_logo setting) with a loop-guarded onerror; recognition
  + recert tiles both covered
- shopfloor dashboard: 'All Business Units' filter label -> 'All Locations'
- geenforce display dispatcher: startup sweep also matches the imaging
  installers' 'GE Aerospace Dashboard/Lobby' shortcuts by name
This commit is contained in:
cproudlock
2026-07-28 18:21:47 -04:00
parent 3a8df166cf
commit 9a2d0ccebb
4 changed files with 63 additions and 25 deletions

View File

@@ -14,7 +14,7 @@
<div class="header-right"> <div class="header-right">
<div class="clock">{{ currentTime }}</div> <div class="clock">{{ currentTime }}</div>
<select v-model="businessUnit" class="filter-select" @change="loadData"> <select v-model="businessUnit" class="filter-select" @change="loadData">
<option value="">All Business Units</option> <option value="">All Locations</option>
<option v-for="bu in businessUnits" :key="bu.businessunitid" :value="bu.businessunitid"> <option v-for="bu in businessUnits" :key="bu.businessunitid" :value="bu.businessunitid">
{{ bu.businessunit }} {{ bu.businessunit }}
</option> </option>
@@ -55,8 +55,8 @@
/> />
<img <img
v-else v-else
:src="siteLogo" :src="employeePhotoFallback"
alt="Site logo" alt="No photo"
class="recognition-photo ge-logo-fallback" class="recognition-photo ge-logo-fallback"
/> />
</div> </div>
@@ -99,8 +99,8 @@
/> />
<img <img
v-else v-else
:src="siteLogo" :src="employeePhotoFallback"
alt="Site logo" alt="No photo"
class="recert-photo ge-logo-fallback" class="recert-photo ge-logo-fallback"
/> />
<div class="recert-name">{{ rec.employeename || rec.employeesso }}</div> <div class="recert-name">{{ rec.employeename || rec.employeesso }}</div>
@@ -193,6 +193,10 @@ import { withBase } from '@/utils/basePath'
const loading = ref(true) const loading = ref(true)
const facilityName = ref('ShopDB') const facilityName = ref('ShopDB')
const siteLogo = ref(withBase('/ge-aerospace-logo.svg')) const siteLogo = ref(withBase('/ge-aerospace-logo.svg'))
// Employee photo placeholder: the shipped GE monogram (square avatar). Kept
// separate from siteLogo so a blank/broken site_logo setting never leaves an
// employee tile with no image.
const employeePhotoFallback = ref(withBase('/ge-monogram.svg'))
// ServiceNow ticket-link config; loaded on mount. Empty/disabled = plain text. // ServiceNow ticket-link config; loaded on mount. Empty/disabled = plain text.
const servicenowConfig = ref({ enabled: true, incidentUrl: '', changeUrl: '' }) const servicenowConfig = ref({ enabled: true, incidentUrl: '', changeUrl: '' })
const businessUnit = ref('') const businessUnit = ref('')
@@ -436,7 +440,11 @@ function getTicketUrl(ticketnumber) {
} }
function handlePhotoError(e) { function handlePhotoError(e) {
e.target.src = siteLogo.value // Photo URL 404'd (e.g. no photo file for this SSO): fall back to the GE
// monogram. Guard against a loop if the fallback itself fails to load.
if (e.target.dataset.fellback) { return }
e.target.dataset.fellback = '1'
e.target.src = employeePhotoFallback.value
e.target.classList.add('ge-logo-fallback') e.target.classList.add('ge-logo-fallback')
} }
</script> </script>

View File

@@ -124,23 +124,41 @@ def resolve_employee_display_name(sso):
None on any miss.""" None on any miss."""
if sso is None or not str(sso).isdigit(): if sso is None or not str(sso).isdigit():
return None return None
# 1. The employee directory (selfhosted table or external wjf_employees).
if _selfhosted(): if _selfhosted():
emp = db.session.get(DirectoryEmployee, int(sso)) emp = db.session.get(DirectoryEmployee, int(sso))
if emp: if emp:
return f'{emp.firstname} {emp.lastname}'.strip() or None name = f'{emp.firstname} {emp.lastname}'.strip()
return None if name:
return name
else:
try:
conn = employee_connection()
with conn.cursor() as cur:
cur.execute(
'SELECT First_Name, Last_Name FROM employees WHERE SSO = %s',
(int(sso),))
row = cur.fetchone()
conn.close()
if row:
first = row.get('First_Name') or ''
last = row.get('Last_Name') or ''
name = f'{first.strip()} {last.strip()}'.strip()
if name:
return name
except Exception:
pass
# 2. Fallback: a shopdb login account for this SSO (e.g. a user from another
# location not carried in the directory). Their name lives on the User
# record; GE usernames are the SSO. Photo still falls back to the GE mark.
try: try:
conn = employee_connection() from shopdb.core.models import User
with conn.cursor() as cur: user = User.query.filter_by(username=str(sso)).first()
cur.execute( if user:
'SELECT First_Name, Last_Name FROM employees WHERE SSO = %s', name = f'{(user.firstname or "").strip()} {(user.lastname or "").strip()}'.strip()
(int(sso),)) if name:
row = cur.fetchone() return name
conn.close()
if row:
first = row.get('First_Name') or ''
last = row.get('Last_Name') or ''
return f'{first.strip()} {last.strip()}'.strip() or None
except Exception: except Exception:
pass pass
return None return None

View File

@@ -152,7 +152,9 @@ $shell = New-Object -ComObject WScript.Shell
# left alone. # left alone.
Get-ChildItem -LiteralPath $startup -Filter '*.lnk' -ErrorAction SilentlyContinue | ForEach-Object {{ Get-ChildItem -LiteralPath $startup -Filter '*.lnk' -ErrorAction SilentlyContinue | ForEach-Object {{
$drop = $false $drop = $false
if ($_.Name -like 'ShopDB Kiosk*') {{ $drop = $true }} if ($_.Name -like 'ShopDB Kiosk*' -or
$_.Name -like 'GE Aerospace Dashboard*' -or
$_.Name -like 'GE Aerospace Lobby*') {{ $drop = $true }}
else {{ else {{
try {{ try {{
$existing = $shell.CreateShortcut($_.FullName) $existing = $shell.CreateShortcut($_.FullName)

View File

@@ -734,10 +734,13 @@ def get_shopfloor_notifications():
else: else:
result['employeesso'] = n.employeesso result['employeesso'] = n.employeesso
# Stored name first (import/manual entry), else resolve live from # Stored name first (import/manual entry), else resolve live from
# the directory so shopdb-only imports still show names. # the directory. Also resolve when the stored "name" is just the bare
# SSO: WJ notifications were imported with SSOs and never converted to
# names, so a digits-only stored name must still be looked up.
name = n.employeename name = n.employeename
if not name and n.employeesso and ',' not in n.employeesso: if (n.employeesso and ',' not in n.employeesso
name = _employee_name(n.employeesso) and (not name or name.strip().isdigit())):
name = _employee_name(n.employeesso) or name
result['employeename'] = name result['employeename'] = name
result['employeepicture'] = _employee_picture(n.employeesso) if show_photo else None result['employeepicture'] = _employee_picture(n.employeesso) if show_photo else None
@@ -754,11 +757,18 @@ def get_shopfloor_notifications():
show_photo = bool(ntype and ntype.showemployeephoto) show_photo = bool(ntype and ntype.showemployeephoto)
ssos = [s.strip() for s in n.employeesso.split(',')] ssos = [s.strip() for s in n.employeesso.split(',')]
names = n.employeename.split(', ') if n.employeename else [] names = n.employeename.split(', ') if n.employeename else []
def _name_for(i, sso):
stored = names[i].strip() if i < len(names) and names[i] else None
# A stored bare SSO (digits) is not a real name - look it up.
if stored and not stored.isdigit():
return stored
return _employee_name(sso) or stored or sso
return [ return [
notification_to_shopfloor(n, { notification_to_shopfloor(n, {
'sso': sso, 'sso': sso,
'name': (names[i] if i < len(names) and names[i] else None) 'name': _name_for(i, sso),
or _employee_name(sso) or sso,
'picture': _employee_picture(sso) if show_photo else None, 'picture': _employee_picture(sso) if show_photo else None,
}) })
for i, sso in enumerate(ssos) for i, sso in enumerate(ssos)