Sep 14 - Update the check-ins pages to force employee to select the type of work

This commit is contained in:
2026-09-14 12:18:53 -04:00
parent d92cff81e5
commit 4fc500707b
4 changed files with 87 additions and 13 deletions
+19 -5
View File
@@ -547,12 +547,15 @@ Toggle UI in `create_qr_code.html` and `edit_qr_code.html` — uses `addEventLis
### Type of Work — Check-In Dropdown (Sept 2026) ### Type of Work — Check-In Dropdown (Sept 2026)
The check-in page carries a **Type of Work / Tipo de Trabajo** `<select>` directly after The check-in page carries a **Type of Work / Tipo de Trabajo** `<select>` directly after
the Employee ID field. The employee enters a **numeric-only** ID and picks the type; the Employee ID field. The employee enters a **numeric-only** ID and picks the type.
`Regular` is selected by default. **There is no default (Sept 14, 2026)** — the dropdown opens on a disabled placeholder
and submit is blocked (red outline + bilingual message) until a type is chosen, because
employees were leaving it on Regular without looking.
| Option value | Label shown (bilingual, one plain string) | | Option value | Label shown (bilingual, one plain string) |
|---|---| |---|---|
| `""` | Regular / Trabajo Regular | | `""` (disabled, selected) | -- Select type of work / Seleccione tipo de trabajo -- |
| `R` | Regular / Trabajo Regular — stored as the plain numeric ID |
| `PW` | Periodic Work / Trabajo Periódico (PW) | | `PW` | Periodic Work / Trabajo Periódico (PW) |
| `SP` | Special Project / Proyecto Especial (SP) | | `SP` | Special Project / Proyecto Especial (SP) |
| `C` | Covering / Cobertura (C) | | `C` | Covering / Cobertura (C) |
@@ -564,7 +567,11 @@ languages share one label separated by `/`.
the format every calculator and export already parses: the format every calculator and export already parses:
```python ```python
work_type = request.form.get('work_type', '').strip().upper() # '' = Regular work_type = request.form.get('work_type', '').strip().upper()
if not work_type:
return jsonify({...}), 400 # nothing chosen — rejected
if work_type == 'R':
work_type = '' # Regular — no code appended
if work_type and work_type not in VALID_CHECKIN_WORK_TYPES: # ('SP','PW','PT','C') if work_type and work_type not in VALID_CHECKIN_WORK_TYPES: # ('SP','PW','PT','C')
return jsonify({...}), 400 # reject unknown codes return jsonify({...}), 400 # reject unknown codes
if employee_id and work_type: if employee_id and work_type:
@@ -670,7 +677,7 @@ load (i.e. after the fetch resolves) and caches plain GETs aggressively:
### Check-In Flow ### Check-In Flow
1. Employee scans QR → `qr_destination.html` 1. Employee scans QR → `qr_destination.html`
2. Enters numeric ID and picks Type of Work (Regular by default); GPS captured by browser 2. Enters numeric ID and **must** pick Type of Work (no default); GPS captured by browser
3. On a Check Out scan the page pre-selects the open check-in's work type 3. On a Check Out scan the page pre-selects the open check-in's work type
4. Work-type code appended to the ID server-side (`1234``1234SP`) 4. Work-type code appended to the ID server-side (`1234``1234SP`)
5. 30-min interval guard (configurable via `TIME_INTERVAL`) 5. 30-min interval guard (configurable via `TIME_INTERVAL`)
@@ -1190,6 +1197,13 @@ exact-match test is what dropped every SP/PW/PT row the query had already return
| `templates/time_attendance_records.html` | Tooltip on the Export by Building button | | `templates/time_attendance_records.html` | Tooltip on the Export by Building button |
| — | Replaces the manual "Copilot" procedure (delete PM rows + SP rows, then build a weekly table). Decisions: PM IDs removed from both sheets; SP hours excluded from weekly hours | | — | Replaces the manual "Copilot" procedure (delete PM rows + SP rows, then build a weekly table). Decisions: PM IDs removed from both sheets; SP hours excluded from weekly hours |
### Set 19 — Type of Work Must Be Selected (Sept 14, 2026)
| File | Change |
|---|---|
| `templates/qr_destination.html` | Disabled placeholder option is the default; Regular value `""``R`; `required`; submit guard + `.work-type-missing` red outline; check-out suggestion maps server `""``R` |
| `static/js/qr_destination.js` | Same guard in `submitCheckin()` |
| `routes/qr_codes.py` | `qr_checkin` rejects an empty `work_type` with HTTP 400; `R` normalised to `''` (Regular). `last-work-type` still returns `""` for Regular |
--- ---
## 21. Infrastructure & Deployment ## 21. Infrastructure & Deployment
+12 -1
View File
@@ -743,8 +743,19 @@ def qr_checkin(qr_url):
# 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
# format ("1234SP") that every calculator and export already parses. # format ("1234SP") that every calculator and export already parses.
# Empty selection = Regular work — the ID is stored unchanged. # The dropdown has no default: 'R' = Regular (ID stored unchanged),
# and an empty value means the employee did not choose — rejected.
work_type = request.form.get('work_type', '').strip().upper() work_type = request.form.get('work_type', '').strip().upper()
if not work_type:
logger_handler.logger.warning(
f"Check-in rejected: no type of work selected for employee {employee_id}"
)
return jsonify({
'success': False,
'message': 'Please select the Type of Work. / Por favor seleccione el Tipo de Trabajo.'
}), 400
if work_type == 'R':
work_type = '' # Regular — no code appended
if work_type and work_type not in VALID_CHECKIN_WORK_TYPES: if work_type and work_type not in VALID_CHECKIN_WORK_TYPES:
logger_handler.logger.warning( logger_handler.logger.warning(
f"Check-in rejected: invalid work type '{work_type}' for employee {employee_id}" f"Check-in rejected: invalid work type '{work_type}' for employee {employee_id}"
+16 -1
View File
@@ -327,10 +327,25 @@ function submitCheckin() {
const workTypeField = document.getElementById("work_type"); const workTypeField = document.getElementById("work_type");
const workType = workTypeField ? workTypeField.value.trim() : ""; const workType = workTypeField ? workTypeField.value.trim() : "";
// Type of Work has no default - the employee must choose one
if (!workType) {
if (workTypeField) {
workTypeField.classList.add("work-type-missing");
workTypeField.focus();
}
showCustomStatusMessage(
"Please select the Type of Work / Por favor seleccione el Tipo de Trabajo",
"error"
);
isSubmitting = false;
updateSubmitButton(false);
return;
}
// Prepare form data // Prepare form data
const formData = new FormData(); const formData = new FormData();
formData.append("employee_id", employeeId); formData.append("employee_id", employeeId);
// Empty string = Regular; PW / SP / C are appended to the ID server-side // R = Regular (ID stored unchanged); PW / SP / C are appended to the ID server-side
formData.append("work_type", workType); formData.append("work_type", workType);
formData.append( formData.append(
"latitude", "latitude",
+39 -5
View File
@@ -69,6 +69,12 @@
margin-right: 6px; margin-right: 6px;
} }
/* Type of Work has no default - flagged when submit is tried without one */
select.work-type-missing {
border: 2px solid #ef4444 !important;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2) !important;
}
.bilingual-container { .bilingual-container {
background: rgba(255, 255, 255, 0.98); background: rgba(255, 255, 255, 0.98);
border-radius: 8px; border-radius: 8px;
@@ -1218,11 +1224,16 @@
name="work_type" name="work_type"
class="form-control" class="form-control"
autocomplete="off" autocomplete="off"
required
> >
<!-- <option> cannot hold the coloured english-text/spanish-text <!-- <option> cannot hold the coloured english-text/spanish-text
spans used elsewhere, so both languages share one plain label, spans used elsewhere, so both languages share one plain label,
separated by "/" like the Employee ID placeholder above. --> separated by "/" like the Employee ID placeholder above.
<option value="" selected>Regular / Trabajo Regular</option> No default: the employee must pick a type before submitting.
Regular is "R" so it is distinguishable from "nothing picked";
the server stores it as the plain numeric ID. -->
<option value="" selected disabled>-- Select type of work / Seleccione tipo de trabajo --</option>
<option value="R">Regular / Trabajo Regular</option>
<option value="PW">Periodic Work / Trabajo Periódico (PW)</option> <option value="PW">Periodic Work / Trabajo Periódico (PW)</option>
<option value="SP">Special Project / Proyecto Especial (SP)</option> <option value="SP">Special Project / Proyecto Especial (SP)</option>
<option value="C">Covering / Cobertura (C)</option> <option value="C">Covering / Cobertura (C)</option>
@@ -1435,6 +1446,7 @@
// Bilingual labels - mirror WORK_TYPE_LABELS in routes/qr_codes.py. // Bilingual labels - mirror WORK_TYPE_LABELS in routes/qr_codes.py.
const WORK_TYPE_LABELS = { const WORK_TYPE_LABELS = {
"": { en: "Regular", es: "Trabajo Regular" }, "": { en: "Regular", es: "Trabajo Regular" },
R: { en: "Regular", es: "Trabajo Regular" },
PW: { en: "Periodic Work", es: "Trabajo Periódico" }, PW: { en: "Periodic Work", es: "Trabajo Periódico" },
SP: { en: "Special Project", es: "Proyecto Especial" }, SP: { en: "Special Project", es: "Proyecto Especial" },
PT: { en: "Project Team", es: "Equipo de Proyecto" }, PT: { en: "Project Team", es: "Equipo de Proyecto" },
@@ -1502,8 +1514,14 @@
return; return;
} }
const field = document.getElementById("work_type"); const field = document.getElementById("work_type");
if (field && field.value !== suggestedWorkTypeCode) { // The server reports an open Regular check-in as ""; the dropdown's
field.value = suggestedWorkTypeCode; // Regular option is "R" ("" is the "nothing selected" placeholder).
const optionValue = suggestedWorkTypeCode === "" ? "R" : suggestedWorkTypeCode;
if (field && field.value !== optionValue) {
field.value = optionValue;
}
if (field && field.value) {
field.classList.remove("work-type-missing");
} }
} }
@@ -1627,6 +1645,9 @@
}); });
field.addEventListener("change", function () { field.addEventListener("change", function () {
if (field.value) {
field.classList.remove("work-type-missing");
}
if (!workTypeTouchedByEmployee) { if (!workTypeTouchedByEmployee) {
// No interaction preceded this change: it is Safari restoring the // No interaction preceded this change: it is Safari restoring the
// control, not the employee choosing. Re-assert the suggestion. // control, not the employee choosing. Re-assert the suggestion.
@@ -1914,6 +1935,19 @@
return; return;
} }
// Type of Work has no default - the employee must choose one
if (!workType) {
if (workTypeField) {
workTypeField.classList.add("work-type-missing");
workTypeField.focus();
}
showStatusMessage(
"Please select the Type of Work / Por favor seleccione el Tipo de Trabajo",
"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);
@@ -1944,7 +1978,7 @@
// Prepare form data with location information // Prepare form data with location information
const formData = new FormData(); const formData = new FormData();
formData.append("employee_id", employeeId); formData.append("employee_id", employeeId);
// Empty string = Regular; PW / SP / C are appended to the ID server-side // R = Regular (ID stored unchanged); PW / SP / C are appended to the ID server-side
formData.append("work_type", workType); formData.append("work_type", workType);
// Read selected location — window globals (most reliable), then hidden field, then dropdown // Read selected location — window globals (most reliable), then hidden field, then dropdown