Sep 15 - Update the check-in pages, employee id field limited to 4 digits/characters
This commit is contained in:
@@ -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`
|
**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
|
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
|
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 —
|
(e.g. `1234SP`) is cleaned to `1234` on auto-fill.
|
||||||
capping at 4 would break any 5-digit employee ID.
|
|
||||||
|
**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)
|
### 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 |
|
| `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 |
|
| — | 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": 1–3 digit IDs still check in. Supersedes the earlier "no `maxlength` cap" note in §11 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 21. Infrastructure & Deployment
|
## 21. Infrastructure & Deployment
|
||||||
|
|||||||
+22
-2
@@ -51,6 +51,11 @@ bp = Blueprint('qr_codes', __name__)
|
|||||||
# PT is accepted for backward compatibility with IDs created before the dropdown.
|
# PT is accepted for backward compatibility with IDs created before the dropdown.
|
||||||
VALID_CHECKIN_WORK_TYPES = ('SP', 'PW', 'PT', 'C')
|
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
|
# 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
|
# 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).
|
# 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
|
# Get and validate employee ID
|
||||||
employee_id = request.form.get('employee_id', '').strip()
|
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 ---
|
# --- ADDED: type of work selected on the check-in page ---
|
||||||
# The employee enters a numeric ID and picks a work type; the code is
|
# 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
|
# 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)
|
return _no_store_json(empty)
|
||||||
|
|
||||||
employee_id = request.args.get('employee_id', '').strip()
|
employee_id = request.args.get('employee_id', '').strip()
|
||||||
# The check-in page allows digits only; anything else cannot be matched.
|
# The check-in page allows up to 4 digits; anything else cannot be matched.
|
||||||
if not employee_id.isdigit():
|
if not employee_id.isdigit() or len(employee_id) > CHECKIN_EMPLOYEE_ID_MAX_DIGITS:
|
||||||
return _no_store_json(empty)
|
return _no_store_json(empty)
|
||||||
|
|
||||||
selected_location_name = request.args.get('selected_location_name', '').strip()
|
selected_location_name = request.args.get('selected_location_name', '').strip()
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
let isSubmitting = false;
|
let isSubmitting = false;
|
||||||
let currentTime = new Date();
|
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
|
// Camera verification variables
|
||||||
let cameraStream = null;
|
let cameraStream = null;
|
||||||
let capturedPhotoData = null;
|
let capturedPhotoData = null;
|
||||||
@@ -97,7 +102,8 @@ function loadLastStaffId() {
|
|||||||
try {
|
try {
|
||||||
const saved = localStorage.getItem("qr_last_staff_id");
|
const saved = localStorage.getItem("qr_last_staff_id");
|
||||||
const digitsOnly = (saved || "").replace(/[^0-9]/g, "");
|
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 digitsOnly;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -135,9 +141,9 @@ function initializeForm() {
|
|||||||
// Add real-time Employee ID validation
|
// Add real-time Employee ID validation
|
||||||
const employeeIdInput = document.getElementById("employee_id");
|
const employeeIdInput = document.getElementById("employee_id");
|
||||||
if (employeeIdInput) {
|
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 () {
|
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) {
|
if (this.value !== digitsOnly) {
|
||||||
this.value = digitsOnly;
|
this.value = digitsOnly;
|
||||||
}
|
}
|
||||||
@@ -275,6 +281,15 @@ function proceedWithCheckin() {
|
|||||||
return;
|
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
|
// Save the staff ID for future use
|
||||||
saveLastStaffId(employeeId);
|
saveLastStaffId(employeeId);
|
||||||
|
|
||||||
@@ -324,6 +339,16 @@ function submitCheckin() {
|
|||||||
return;
|
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 workTypeField = document.getElementById("work_type");
|
||||||
const workType = workTypeField ? workTypeField.value.trim() : "";
|
const workType = workTypeField ? workTypeField.value.trim() : "";
|
||||||
|
|
||||||
|
|||||||
@@ -1203,6 +1203,7 @@
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
pattern="[0-9]*"
|
pattern="[0-9]*"
|
||||||
|
maxlength="4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1666,6 +1667,12 @@
|
|||||||
loadOpenCheckInWorkType();
|
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
|
// FIXED Employee ID auto-fill functionality
|
||||||
function initializeEmployeeIdAutoFill() {
|
function initializeEmployeeIdAutoFill() {
|
||||||
const employeeIdInput = document.getElementById("employee_id");
|
const employeeIdInput = document.getElementById("employee_id");
|
||||||
@@ -1681,7 +1688,9 @@
|
|||||||
localStorage.getItem("qr_last_employee_id") || ""
|
localStorage.getItem("qr_last_employee_id") || ""
|
||||||
).replace(/[^0-9]/g, "");
|
).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;
|
employeeIdInput.value = lastEmployeeId;
|
||||||
|
|
||||||
// Visual feedback that it's auto-filled
|
// Visual feedback that it's auto-filled
|
||||||
@@ -1702,10 +1711,11 @@
|
|||||||
function attachEmployeeIdListeners(employeeIdInput) {
|
function attachEmployeeIdListeners(employeeIdInput) {
|
||||||
if (!employeeIdInput) return;
|
if (!employeeIdInput) return;
|
||||||
|
|
||||||
// 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
|
||||||
// (covers paste, autofill and keyboards that ignore inputmode).
|
// it is typed (covers paste, autofill and keyboards that ignore
|
||||||
|
// inputmode; maxlength alone does not limit values set by script).
|
||||||
employeeIdInput.addEventListener("input", function () {
|
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) {
|
if (this.value !== digitsOnly) {
|
||||||
this.value = digitsOnly;
|
this.value = digitsOnly;
|
||||||
}
|
}
|
||||||
@@ -1914,6 +1924,15 @@
|
|||||||
return;
|
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
|
// Save Employee ID to localStorage for next time
|
||||||
try {
|
try {
|
||||||
localStorage.setItem("qr_last_employee_id", employeeId);
|
localStorage.setItem("qr_last_employee_id", employeeId);
|
||||||
|
|||||||
Reference in New Issue
Block a user