Files
shopdb-flask/plugins/employees
cproudlock 22e623c1f6 Plugin framework maturation, reports overhaul, theming, and USB frontend repair
Framework:
- Per-plugin Alembic migration chains (ADR-008): every bundled plugin
  carries its own chain with a stamp-only anchor at the ownership cutover;
  new plugin schema lands in plugins/<name>/migrations/, never the core
  chain. Deploys add flask plugin upgrade-all. Fixed a latent bug in the
  shared alembic template (engine URL resolution) and taught the metadata
  filter to include FK-referenced core tables.
- Frontend plugin route gating (ADR-009): plugin routes carry meta.plugin;
  a disabled plugin's pages redirect to the dashboard via a cached,
  fail-open check against the new public GET /api/plugins/enabled.
- get_reports() plugin hook (contract 0.5.0 -> 0.6.0): plugins contribute
  report cards; warranty and toner cards moved off the hardcoded list.

Reports:
- Hub grouped by category with search; inline reports render at the top,
  are URL-backed (?report=id, back-button and deep links work), expose
  their server-side filter params as controls, and export CSV. Warranty
  and Toner pages gained CSV export.
- Deleted the dead legacy Warranty Status report (always-zero buckets
  from a retired column).

Theming and fonts:
- Inter (variable) bundled locally via @fontsource, replacing the Google
  Fonts Roboto import - air-gapped installs now render correctly; tables
  use tabular numerals.
- Optional brand_primary_dark_color, brand_accent_color,
  brand_sidebar_color settings applied to CSS vars at bootstrap.

USB frontend repair (views were reading a dead legacy shape):
- List/detail/form and the employee profile USB panels remapped to the
  real API shape (device_id/device_desc/checkinoutlog); employee panels
  now use /usb/checkouts endpoints; external-mode /usb/checkouts/active
  honors the badge filter; dead client methods pruned.

Also: warranties list page no longer requires login (matches app
convention); collector doc rewritten with a GE-Enforce integration guide
and paste-ready PowerShell reporter; ADR index and CHANGELOG updated.

Verified: 323 tests pass, naming/style green, frontend builds, plugin
migration dry-run green on scratch MySQL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 10:01:47 -04:00
..

Employees plugin - directory database contract

Read-only lookups against a separate employee/HR directory database. Powers:

  • Employee search (add people to a notification, browse the directory)
  • Single + batch SSO lookup
  • Recognition notifications (name + photo on the shopfloor / lobby displays)

The plugin never writes to this database. Use a read-only account.

This is the integration most likely to differ per site. HR / directory systems vary widely, so expect to map a site's schema to the contract below - the CREATE VIEW recipe at the end is the normal way to do it.

Connection

Credentials resolve settings-first, then environment, except the password which is env-only (never stored in the app database).

Field Setting key (editable in the setup wizard) Env var (fallback)
Host employee_db_host EMPLOYEE_DB_HOST
Database employee_db_name EMPLOYEE_DB_NAME
User employee_db_user EMPLOYEE_DB_USER
Password (not stored) EMPLOYEE_DB_PASSWORD

Set the password in .env; the setup wizard's Features step shows the exact line to paste. Engine is MySQL/MariaDB (pymysql).

Required schema

The plugin runs these queries verbatim, so a site's database must expose a table (or view - see below) named employees with these columns:

Column Type Notes
SSO INT Unique person id. Lookups require it to be numeric
First_Name VARCHAR Displayed as the given name
Last_Name VARCHAR Displayed as the surname; sort key
Team VARCHAR Team / group label
Role VARCHAR Job title / role
Picture VARCHAR Image filename (see Photos)

Queries actually executed:

-- search
SELECT SSO, First_Name, Last_Name, Team, Role, Picture
FROM employees
WHERE First_Name LIKE %s OR Last_Name LIKE %s OR CAST(SSO AS CHAR) LIKE %s
ORDER BY Last_Name, First_Name LIMIT %s;

-- single / batch
SELECT SSO, First_Name, Last_Name, Team, Role, Picture FROM employees WHERE SSO = %s;
SELECT SSO, First_Name, Last_Name, Team, Role, Picture FROM employees WHERE SSO IN (...);

Rows are returned to the API with these exact column names. The frontend (EmployeeSearch, NotificationForm, EmployeeDetail, ShopfloorDashboard) reads SSO, First_Name, Last_Name, Team, Role, Picture as-is - do not rename them in the response.

Photos

Picture holds an image filename (e.g. 123456.jpg), not a path or blob. The app renders it as /static/employees/<Picture>, so the image files must live in the app's static/employees/ directory. Leave Picture empty/NULL for people with no photo; the UI falls back to initials.

Two ways to provide the directory

Option A - map an existing HR/directory database (see the view recipe below). Use this when the site already has a system of record for people.

Option B - self-hosted directory (managed in-app) for sites with no HR database. Set employee_directory_mode to selfhosted and manage people under Settings > Employee Directory (add / edit / delete + CSV import) - no SQL needed. The app owns a directoryemployees table (migration 7d16); the lookup APIs read it automatically in this mode.

If you would rather load it with SQL, the canonical table is equivalent to:

CREATE DATABASE shopdb_directory CHARACTER SET utf8mb4;
USE shopdb_directory;

CREATE TABLE employees (
  SSO         INT          NOT NULL PRIMARY KEY,
  First_Name  VARCHAR(100) NOT NULL,
  Last_Name   VARCHAR(100) NOT NULL,
  Team        VARCHAR(100) NULL,
  Role        VARCHAR(100) NULL,
  Picture     VARCHAR(255) NULL
);

-- add people (or bulk-load from CSV with LOAD DATA INFILE)
INSERT INTO employees (SSO, First_Name, Last_Name, Team, Role, Picture)
VALUES (123456, 'Jane', 'Doe', 'Inspection', 'Quality Tech', '123456.jpg');

Put photo files (named as in Picture) under the app's static/employees/. In self-hosted mode the directoryemployees table lives in the main app DB, so no separate database is required.

Sites whose HR/directory database uses different table or column names should not be forced to rename anything. Instead, create a read-only view named employees that maps local columns to the names above:

CREATE VIEW employees AS
SELECT
  person_id        AS SSO,
  given_name       AS First_Name,
  surname          AS Last_Name,
  department       AS Team,
  job_title        AS Role,
  photo_filename   AS Picture
FROM hr_people
WHERE active = 1;

Grant the app's read-only user SELECT on the view. No app code changes - point employee_db_* at that database and the plugin works.

Notes:

  • SSO must be numeric (single/batch lookup validate isdigit()).
  • Column-name case follows your database's identifier casing; match the names above exactly on case-sensitive platforms.
  • If the directory is unreachable or the employees object is missing, lookups return a 500 and the rest of the app keeps working (the feature degrades, it does not crash the app).