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>
This commit is contained in:
@@ -9,6 +9,46 @@ from shopdb.utils.responses import success_response, error_response, ErrorCodes
|
||||
auditlogs_bp = Blueprint('auditlogs', __name__)
|
||||
|
||||
|
||||
def _resolve_full_names(ssos):
|
||||
"""Best-effort SSO -> full name map for audit rows. Mode-aware, guarded,
|
||||
degrades to {} so the list still renders if the directory is unreachable."""
|
||||
wanted = {s for s in ssos if s and str(s).isdigit()}
|
||||
if not wanted:
|
||||
return {}
|
||||
names = {}
|
||||
# Self-hosted directory first (no external dependency).
|
||||
try:
|
||||
from plugins.employees.models import DirectoryEmployee
|
||||
rows = DirectoryEmployee.query.filter(
|
||||
DirectoryEmployee.sso.in_(wanted)).all()
|
||||
for emp in rows:
|
||||
full = f'{(emp.firstname or "").strip()} {(emp.lastname or "").strip()}'.strip()
|
||||
if full:
|
||||
names[str(emp.sso)] = full
|
||||
except Exception:
|
||||
pass
|
||||
missing = wanted - set(names)
|
||||
if not missing:
|
||||
return names
|
||||
# External HR directory for anything still unresolved.
|
||||
try:
|
||||
from shopdb.utils.employee_db import employee_connection
|
||||
conn = employee_connection()
|
||||
placeholders = ','.join(['%s'] * len(missing))
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
'SELECT SSO, First_Name, Last_Name FROM employees '
|
||||
f'WHERE SSO IN ({placeholders})', tuple(missing))
|
||||
for emp in cur.fetchall():
|
||||
full = f'{(emp["First_Name"] or "").strip()} {(emp["Last_Name"] or "").strip()}'.strip()
|
||||
if full:
|
||||
names[str(emp['SSO'])] = full
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
return names
|
||||
|
||||
|
||||
@auditlogs_bp.route('', methods=['GET'])
|
||||
@jwt_required()
|
||||
def list_auditlogs():
|
||||
@@ -75,8 +115,14 @@ def list_auditlogs():
|
||||
# Paginate
|
||||
pagination = query.paginate(page=page, per_page=perpage, error_out=False)
|
||||
|
||||
rows = [log.to_dict() for log in pagination.items]
|
||||
# Attach full names (hover tooltip on the SSO); best-effort.
|
||||
fullnames = _resolve_full_names({r.get('username') for r in rows})
|
||||
for r in rows:
|
||||
r['userfullname'] = fullnames.get(str(r.get('username')))
|
||||
|
||||
return success_response(
|
||||
[log.to_dict() for log in pagination.items],
|
||||
rows,
|
||||
meta={
|
||||
'page': page,
|
||||
'perpage': perpage,
|
||||
|
||||
@@ -434,6 +434,13 @@ def build_default_settings():
|
||||
'category': 'site',
|
||||
'description': 'Template for a printer hostname built from its IP. {ip} is the dash-separated IP address.'
|
||||
},
|
||||
{
|
||||
'key': 'contact_email_domain',
|
||||
'value': 'geaerospace.com',
|
||||
'valuetype': 'string',
|
||||
'category': 'site',
|
||||
'description': 'Email domain appended to a contact SSO to build email/Teams links (sso@domain). Blank disables contact action buttons.'
|
||||
},
|
||||
]
|
||||
|
||||
# Site branding. Each instance (ADR-004) can replace the shipped GE defaults
|
||||
|
||||
Reference in New Issue
Block a user