Files
shopdb-flask/plugins/employees
cproudlock ed5d64fa3e Name the record in the browser tab, not just the section
Every detail page read "West Jefferson ShopDB - Machine". Two tabs on two
different machines were indistinguishable, a bookmark said nothing about what
it pointed at, and a link shared into Teams previewed as the section rather
than the thing.

The router cannot fix this alone: afterEach runs before the view has fetched
anything, so it only ever knows the KIND of page. So a detail view names its
record once the record arrives, and both writers go through
composables/pageTitle.js.

THE ORDERING IS THE WHOLE PROBLEM. The router awaits the site settings to build
the "<facility> ShopDB" prefix, so on a cold load it finishes AFTER a view that
fetched quickly and would overwrite the good title with the generic one. The
route's fullPath is claimed before that await, and the generic write stands
down when a record title already belongs to the route being landed on. The
reverse case is handled too: a view that names its record BEFORE the facility
name arrives composed against the placeholder prefix, so the late resolve
rewrites it rather than leaving "ShopDB - CSF16" on a site called West
Jefferson. Both are pinned by tests, because both fail only under timing.

Covers machines, PCs, printers, network devices, subnets, measuring tools, USB
devices, printed parts, applications, employees, knowledge base articles and
guides. Printers reuse the page's own displayTitle, which already discards the
literal name "NONE" a discovered queue can carry, so the tab and the heading
agree.

A record with no usable name, or one that failed to load, keeps the section
name: "Machine" beats "Machine 4192" for somebody scanning a row of tabs.
Long names are cut at a word boundary - knowledge base articles are named with
a whole sentence of description, which a browser otherwise truncates mid-word.

Reports gained meta.title, which the router already supported and nothing used.
That also fixes "Pc Relationships", which the name-derived spelling produced.
2026-08-26 13:13:30 -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).