Shopfloor feed resolves employee names live when none is stored
Photos already resolved through the directory at read time, but names only came from the stored employeename column - empty after a shopdb-only import, so recertification/recognition cards showed bare SSOs. New resolve_employee_display_name in the employees plugin (mode-aware: self-hosted table or external HR) backs a fallback in both the single-card and split-per-employee paths; stored names still win when present.
This commit is contained in:
@@ -116,6 +116,36 @@ def _hr_picture(sso):
|
||||
return None
|
||||
|
||||
|
||||
def resolve_employee_display_name(sso):
|
||||
"""Display name ("First Last") for an SSO in either directory mode.
|
||||
|
||||
The shopfloor feed uses this as a live fallback when a notification has
|
||||
no stored employeename (e.g. imported without the employee source).
|
||||
None on any miss."""
|
||||
if sso is None or not str(sso).isdigit():
|
||||
return None
|
||||
if _selfhosted():
|
||||
emp = db.session.get(DirectoryEmployee, int(sso))
|
||||
if emp:
|
||||
return f'{emp.firstname} {emp.lastname}'.strip() or None
|
||||
return None
|
||||
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 ''
|
||||
return f'{first.strip()} {last.strip()}'.strip() or None
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def resolve_employee_photo_url(sso, external_picture=None):
|
||||
"""Single resolver both consumers share: the display photo URL for an SSO.
|
||||
|
||||
|
||||
@@ -149,6 +149,16 @@ def _config_version():
|
||||
return hashlib.md5('||'.join(parts).encode()).hexdigest()[:12]
|
||||
|
||||
|
||||
def _employee_name(sso):
|
||||
"""Live directory name for an SSO; None on any miss. Used as the fallback
|
||||
when a notification has no stored employeename (see the shopfloor feed)."""
|
||||
try:
|
||||
from plugins.employees.api.routes import resolve_employee_display_name
|
||||
return resolve_employee_display_name(sso)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _employee_picture(sso):
|
||||
"""Resolved display photo URL for an SSO, via the shared employees-plugin
|
||||
resolver so kiosk cards match EmployeeDetail in both directory modes
|
||||
@@ -723,7 +733,12 @@ def get_shopfloor_notifications():
|
||||
result['employeepicture'] = employee_override.get('picture')
|
||||
else:
|
||||
result['employeesso'] = n.employeesso
|
||||
result['employeename'] = n.employeename
|
||||
# Stored name first (import/manual entry), else resolve live from
|
||||
# the directory so shopdb-only imports still show names.
|
||||
name = n.employeename
|
||||
if not name and n.employeesso and ',' not in n.employeesso:
|
||||
name = _employee_name(n.employeesso)
|
||||
result['employeename'] = name
|
||||
result['employeepicture'] = _employee_picture(n.employeesso) if show_photo else None
|
||||
|
||||
return result
|
||||
@@ -742,7 +757,8 @@ def get_shopfloor_notifications():
|
||||
return [
|
||||
notification_to_shopfloor(n, {
|
||||
'sso': sso,
|
||||
'name': names[i] if i < len(names) else sso,
|
||||
'name': (names[i] if i < len(names) and names[i] else None)
|
||||
or _employee_name(sso) or sso,
|
||||
'picture': _employee_picture(sso) if show_photo else None,
|
||||
})
|
||||
for i, sso in enumerate(ssos)
|
||||
|
||||
@@ -70,3 +70,32 @@ def test_single_employee_recognition_stays_single_card(client, db):
|
||||
current = resp.get_json()['data']['current']
|
||||
assert len(current) == 1
|
||||
assert current[0]['employeesso'] == '111'
|
||||
|
||||
|
||||
def test_shopfloor_names_resolve_live_when_not_stored(client, app, db):
|
||||
"""A notification imported without employeename shows the directory name,
|
||||
not the bare SSO - single and split-per-employee paths both."""
|
||||
from plugins.employees.models import DirectoryEmployee
|
||||
from plugins.notifications.models import Notification, NotificationType
|
||||
from shopdb.core.models import Setting
|
||||
|
||||
with app.app_context():
|
||||
Setting.set('employee_directory_mode', 'selfhosted',
|
||||
valuetype='string', category='employees')
|
||||
db.session.add(DirectoryEmployee(
|
||||
sso=502000777, firstname='Recert', lastname='Person'))
|
||||
ntype = NotificationType(typename='Recertification',
|
||||
typecolor='recertification',
|
||||
splitperemployee=True)
|
||||
db.session.add(ntype)
|
||||
db.session.flush()
|
||||
db.session.add(Notification(
|
||||
notificationtypeid=ntype.notificationtypeid,
|
||||
notification='Recert due', isshopfloor=True,
|
||||
employeesso='502000777', employeename=None))
|
||||
db.session.commit()
|
||||
|
||||
feed = client.get('/api/notifications/shopfloor').get_json()['data']
|
||||
cards = feed['current'] + feed['upcoming']
|
||||
card = next(c for c in cards if c['notification'] == 'Recert due')
|
||||
assert card['employeename'] == 'Recert Person'
|
||||
|
||||
Reference in New Issue
Block a user