Sep 15 - Update the check-in pages, employee id field limited to 4 digits/characters

This commit is contained in:
2026-09-15 12:53:06 -04:00
parent e5410d5141
commit b33ed8d6c8
4 changed files with 94 additions and 11 deletions
+21 -2
View File
@@ -586,8 +586,19 @@ source of the bilingual labels echoed back to the page.
**Employee ID is numeric only**`inputmode="numeric"`, `pattern="[0-9]*"`, an `input`
listener stripping non-digits (paste / autofill), a re-strip at submit, and a
digits-only guard before the POST. A `localStorage` value stored before this rule
(e.g. `1234SP`) is cleaned to `1234` on auto-fill. There is **no** `maxlength` cap —
capping at 4 would break any 5-digit employee ID.
(e.g. `1234SP`) is cleaned to `1234` on auto-fill.
**Employee ID is at most 4 digits (Sept 15, 2026, user decision)**`maxlength="4"`, plus
`.slice(0, 4)` in both `input` listeners (script-set values ignore `maxlength`), a bilingual
guard on both submit paths, and an HTTP 400 in `qr_checkin` when the base ID has more than
4 digits; `last-work-type` stays quiet for longer IDs. Shorter IDs are still accepted. Longer
IDs are **refused, never truncated** (a cut-down ID is a different employee), and a stored ID
longer than 4 digits is **not** auto-filled. **An employee with a 5+ digit ID cannot check in**
if that changes, raise all four together: the `maxlength` attribute, `EMPLOYEE_ID_MAX_DIGITS`
(`qr_destination.html`), `QR_EMPLOYEE_ID_MAX_DIGITS` (`qr_destination.js`) and
`CHECKIN_EMPLOYEE_ID_MAX_DIGITS` (`routes/qr_codes.py`). The two JS constants have different
names on purpose: both scripts share the page's global scope, and a duplicate top-level
`const` is a SyntaxError that would break the whole page.
### Type of Work — Anti-Mistake Measures (Sept 2026)
@@ -1246,6 +1257,14 @@ it (the workers share no pub/sub).
| `static/js/attendance_report.js` | **Security:** `createTableRow()` put raw device / address / name text into `innerHTML`. Device comes from the check-in User-Agent, so a crafted UA could inject markup into the report — every value is now escaped |
| — | Verified: filter helper produces identical SQL + params to the previous inline block for 180 input combinations; `_live_record_payload()` matches what `loadTableData()` reads from the Jinja-rendered `<tbody>` for 20 row variants; the real `attendance_report.js` driven with a fake DOM/timers/fetch passes 26 checks (insert, de-dup, page + sort kept, hidden-tab pause, no overlap, backoff + cap, stop on logout, escaping). Not yet exercised against a live MySQL server or a real browser |
### Set 21 — Check-In Employee ID Limited to 4 Digits (Sept 15, 2026)
| File | Change |
|---|---|
| `templates/qr_destination.html` | `maxlength="4"`; `EMPLOYEE_ID_MAX_DIGITS`; input listener cuts to 4 digits; stored IDs over 4 digits not auto-filled; submit guard with bilingual message |
| `static/js/qr_destination.js` | `QR_EMPLOYEE_ID_MAX_DIGITS`; same cut in `initializeForm()`; `loadLastStaffId()` ignores longer IDs; guards in `handleFormSubmit()` and `submitCheckin()` |
| `routes/qr_codes.py` | `CHECKIN_EMPLOYEE_ID_MAX_DIGITS`; `qr_checkin` rejects a base ID over 4 digits (HTTP 400, bilingual); `qr_last_work_type` returns no suggestion for longer IDs |
| — | "Up to 4 digits" chosen over "exactly 4": 13 digit IDs still check in. Supersedes the earlier "no `maxlength` cap" note in §11 |
---
## 21. Infrastructure & Deployment
+22 -2
View File
@@ -51,6 +51,11 @@ bp = Blueprint('qr_codes', __name__)
# PT is accepted for backward compatibility with IDs created before the dropdown.
VALID_CHECKIN_WORK_TYPES = ('SP', 'PW', 'PT', 'C')
# Employee IDs entered on the check-in page are at most 4 digits. Keep in sync
# with the maxlength on #employee_id, EMPLOYEE_ID_MAX_DIGITS in
# qr_destination.html and QR_EMPLOYEE_ID_MAX_DIGITS in qr_destination.js.
CHECKIN_EMPLOYEE_ID_MAX_DIGITS = 4
# Bilingual labels for each work type, echoed back to the check-in page so the
# submit button, the success card, and the check-out reminder all name the type
# the same way the dropdown does. Keyed by code; '' is Regular (no code stored).
@@ -739,6 +744,21 @@ def qr_checkin(qr_url):
# Get and validate employee ID
employee_id = request.form.get('employee_id', '').strip()
# At most 4 digits — counted on the base ID, so an old-style typed
# suffix ("1234SP") from a page cached before the numeric-only rule
# still passes. Refused, never truncated: a shorter ID is another person.
if employee_id:
base_for_length, _ = parse_employee_id_for_work_type(employee_id)
if len(re.sub(r'\D', '', base_for_length)) > CHECKIN_EMPLOYEE_ID_MAX_DIGITS:
logger_handler.logger.warning(
f"Check-in rejected: employee ID '{employee_id}' has more than "
f"{CHECKIN_EMPLOYEE_ID_MAX_DIGITS} digits (QR {qr_url})"
)
return jsonify({
'success': False,
'message': 'Employee ID must be 4 digits or fewer. / El ID de empleado debe tener 4 dígitos o menos.'
}), 400
# --- ADDED: type of work selected on the check-in page ---
# The employee enters a numeric ID and picks a work type; the code is
# appended to the ID so the stored value keeps the existing storage
@@ -1234,8 +1254,8 @@ def qr_last_work_type(qr_url):
return _no_store_json(empty)
employee_id = request.args.get('employee_id', '').strip()
# The check-in page allows digits only; anything else cannot be matched.
if not employee_id.isdigit():
# The check-in page allows up to 4 digits; anything else cannot be matched.
if not employee_id.isdigit() or len(employee_id) > CHECKIN_EMPLOYEE_ID_MAX_DIGITS:
return _no_store_json(empty)
selected_location_name = request.args.get('selected_location_name', '').strip()
+28 -3
View File
@@ -7,6 +7,11 @@
let isSubmitting = false;
let currentTime = new Date();
// Employee IDs are at most 4 digits. Keep in sync with the maxlength on
// #employee_id, EMPLOYEE_ID_MAX_DIGITS in qr_destination.html and
// CHECKIN_EMPLOYEE_ID_MAX_DIGITS in routes/qr_codes.py.
const QR_EMPLOYEE_ID_MAX_DIGITS = 4;
// Camera verification variables
let cameraStream = null;
let capturedPhotoData = null;
@@ -97,7 +102,8 @@ function loadLastStaffId() {
try {
const saved = localStorage.getItem("qr_last_staff_id");
const digitsOnly = (saved || "").replace(/[^0-9]/g, "");
if (digitsOnly.length >= 2) {
// Longer stored IDs are not filled in: cutting them would be another employee
if (digitsOnly.length >= 2 && digitsOnly.length <= QR_EMPLOYEE_ID_MAX_DIGITS) {
return digitsOnly;
}
return null;
@@ -135,9 +141,9 @@ function initializeForm() {
// Add real-time Employee ID validation
const employeeIdInput = document.getElementById("employee_id");
if (employeeIdInput) {
// Employee ID is numeric only — strip anything else as it is typed
// Employee ID is numeric only, at most 4 digits — strip anything else as it is typed
employeeIdInput.addEventListener("input", function () {
const digitsOnly = this.value.replace(/[^0-9]/g, "");
const digitsOnly = this.value.replace(/[^0-9]/g, "").slice(0, QR_EMPLOYEE_ID_MAX_DIGITS);
if (this.value !== digitsOnly) {
this.value = digitsOnly;
}
@@ -275,6 +281,15 @@ function proceedWithCheckin() {
return;
}
// Refuse rather than truncate: a cut-down ID is a different employee
if (employeeId.length > QR_EMPLOYEE_ID_MAX_DIGITS) {
showCustomStatusMessage(
"Employee ID must be 4 digits or fewer / El ID de empleado debe tener 4 dígitos o menos",
"error"
);
return;
}
// Save the staff ID for future use
saveLastStaffId(employeeId);
@@ -324,6 +339,16 @@ function submitCheckin() {
return;
}
if (employeeId.length > QR_EMPLOYEE_ID_MAX_DIGITS) {
showCustomStatusMessage(
"Employee ID must be 4 digits or fewer / El ID de empleado debe tener 4 dígitos o menos",
"error"
);
isSubmitting = false;
updateSubmitButton(false);
return;
}
const workTypeField = document.getElementById("work_type");
const workType = workTypeField ? workTypeField.value.trim() : "";
+23 -4
View File
@@ -1203,6 +1203,7 @@
autocomplete="off"
inputmode="numeric"
pattern="[0-9]*"
maxlength="4"
/>
</div>
@@ -1666,6 +1667,12 @@
loadOpenCheckInWorkType();
}
// Employee IDs are at most 4 digits. Keep in sync with the maxlength on
// #employee_id, QR_EMPLOYEE_ID_MAX_DIGITS in qr_destination.js and
// CHECKIN_EMPLOYEE_ID_MAX_DIGITS in routes/qr_codes.py. (Distinct names:
// both scripts share the page's global scope.)
const EMPLOYEE_ID_MAX_DIGITS = 4;
// FIXED Employee ID auto-fill functionality
function initializeEmployeeIdAutoFill() {
const employeeIdInput = document.getElementById("employee_id");
@@ -1681,7 +1688,9 @@
localStorage.getItem("qr_last_employee_id") || ""
).replace(/[^0-9]/g, "");
if (lastEmployeeId !== "") {
// A stored ID longer than the limit is not filled in: cutting it
// down would silently check in a different employee.
if (lastEmployeeId !== "" && lastEmployeeId.length <= EMPLOYEE_ID_MAX_DIGITS) {
employeeIdInput.value = lastEmployeeId;
// Visual feedback that it's auto-filled
@@ -1702,10 +1711,11 @@
function attachEmployeeIdListeners(employeeIdInput) {
if (!employeeIdInput) return;
// Employee ID is numeric only — strip anything else as it is typed
// (covers paste, autofill and keyboards that ignore inputmode).
// Employee ID is numeric only, at most 4 digits — strip anything else as
// it is typed (covers paste, autofill and keyboards that ignore
// inputmode; maxlength alone does not limit values set by script).
employeeIdInput.addEventListener("input", function () {
const digitsOnly = this.value.replace(/[^0-9]/g, "");
const digitsOnly = this.value.replace(/[^0-9]/g, "").slice(0, EMPLOYEE_ID_MAX_DIGITS);
if (this.value !== digitsOnly) {
this.value = digitsOnly;
}
@@ -1914,6 +1924,15 @@
return;
}
// Refuse rather than truncate: a cut-down ID is a different employee
if (employeeId.length > EMPLOYEE_ID_MAX_DIGITS) {
showStatusMessage(
"Employee ID must be 4 digits or fewer / El ID de empleado debe tener 4 dígitos o menos",
"error"
);
return;
}
// Save Employee ID to localStorage for next time
try {
localStorage.setItem("qr_last_employee_id", employeeId);