Add USB, Notifications, Network plugins and reusable EmployeeSearch component

New Plugins:
- USB plugin: Device checkout/checkin with employee lookup, checkout history
- Notifications plugin: Announcements with types, scheduling, shopfloor display
- Network plugin: Network device management with subnets and VLANs
- Equipment and Computers plugins: Asset type separation

Frontend:
- EmployeeSearch component: Reusable employee lookup with autocomplete
- USB views: List, detail, checkout/checkin modals
- Notifications views: List, form with recognition mode
- Network views: Device list, detail, form
- Calendar view with FullCalendar integration
- Shopfloor and TV dashboard views
- Reports index page
- Map editor for asset positioning
- Light/dark mode fixes for map tooltips

Backend:
- Employee search API with external lookup service
- Collector API for PowerShell data collection
- Reports API endpoints
- Slides API for TV dashboard
- Fixed AppVersion model (removed BaseModel inheritance)
- Added checkout_name column to usbcheckouts table

Styling:
- Unified detail page styles
- Improved pagination (page numbers instead of prev/next)
- Dark/light mode theme improvements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-01-21 16:37:49 -05:00
parent 02d83335ee
commit 9c220a4194
110 changed files with 17693 additions and 600 deletions

View File

@@ -0,0 +1,104 @@
"""Employee lookup service - queries wjf_employees database."""
from typing import Optional, Dict, List
import pymysql
from flask import current_app
def get_employee_connection():
"""Get connection to wjf_employees database."""
return pymysql.connect(
host='localhost',
user='root',
password='rootpassword',
database='wjf_employees',
cursorclass=pymysql.cursors.DictCursor
)
def lookup_employee(sso: str) -> Optional[Dict]:
"""
Look up employee by SSO.
Returns dict with: SSO, First_Name, Last_Name, full_name, Picture, etc.
"""
if not sso or not sso.strip().isdigit():
return None
try:
conn = get_employee_connection()
with conn.cursor() as cur:
cur.execute(
'SELECT * FROM employees WHERE SSO = %s',
(int(sso.strip()),)
)
row = cur.fetchone()
if row:
# Add computed full_name
first = (row.get('First_Name') or '').strip()
last = (row.get('Last_Name') or '').strip()
row['full_name'] = f"{first} {last}".strip()
return row
conn.close()
except Exception as e:
current_app.logger.error(f"Employee lookup error: {e}")
return None
def lookup_employees(sso_list: str) -> List[Dict]:
"""
Look up multiple employees by comma-separated SSO list.
Returns list of employee dicts.
"""
if not sso_list:
return []
ssos = [s.strip() for s in sso_list.split(',') if s.strip().isdigit()]
if not ssos:
return []
try:
conn = get_employee_connection()
with conn.cursor() as cur:
placeholders = ','.join(['%s'] * len(ssos))
cur.execute(
f'SELECT * FROM employees WHERE SSO IN ({placeholders})',
[int(s) for s in ssos]
)
rows = cur.fetchall()
# Add computed full_name to each
for row in rows:
first = (row.get('First_Name') or '').strip()
last = (row.get('Last_Name') or '').strip()
row['full_name'] = f"{first} {last}".strip()
return rows
conn.close()
except Exception as e:
current_app.logger.error(f"Employee lookup error: {e}")
return []
def get_employee_names(sso_list: str) -> str:
"""
Get comma-separated list of employee names from SSO list.
Input: "212574611,212637451"
Output: "Brandon Saltz, Jon Kolkmann"
"""
employees = lookup_employees(sso_list)
if not employees:
return sso_list # Return SSOs as fallback
return ', '.join(emp['full_name'] for emp in employees if emp.get('full_name'))
def get_employee_picture_url(sso: str) -> Optional[str]:
"""Get URL to employee picture if available."""
emp = lookup_employee(sso)
if emp and emp.get('Picture'):
# Pictures are stored relative paths like "Support/212574611.png"
return f"/static/employees/{emp['Picture']}"
return None