From 3dd98a7cac95a3b05ad1ef66a3d5d3e2f199d9fb Mon Sep 17 00:00:00 2001 From: NguyenND Date: Fri, 18 Sep 2026 12:48:38 -0400 Subject: [PATCH] Sep 18 - Update dashboard, PMs will see the QR codes of their assigned projects/buidlings only --- Claude.md | 14 ++++++++++++++ routes/dashboard.py | 15 ++++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Claude.md b/Claude.md index 38bc201..f2c1a6b 100644 --- a/Claude.md +++ b/Claude.md @@ -218,6 +218,12 @@ no assignments, or a permission lookup error, sees nothing). Applied in: `time_attendance_locations_api`, `search_employees_api`, `get_project_locations_api` - `routes/dashboard.py`: dashboard QR list + project list, `project_qr_codes` (403-style redirect for someone else's project), `dashboard_stats_api`, `dashboard_realtime_api` +- **Projects and locations combine with AND, never OR** (Set 28) — assigned projects, narrowed to + the assigned locations when there are any. The report has always worked this way + (`_attendance_filter_conditions`); the dashboard used OR, so a PM assigned location "Bldg A" + also saw every OTHER project's QR codes that used that same location name. + A PM with locations but **no** project assignment still sees those locations across projects — + that is the only way such a user sees anything at all - A PM scoped only by **locations** is resolved to the projects behind those locations when employee names are searched, so their filter still works without exposing other projects. - **Any new endpoint returning attendance, employee, QR or project rows must call it.** @@ -1417,6 +1423,14 @@ it (the workers share no pub/sub). | `app.py` | `CURRENT_YEAR` from local time | | — | Verified offline (42 checks): 16 date cases + 6 rejections, partial-batch rollback against a fake session, PM scope loading, scoped vs unscoped SQL for both locations APIs (values bound as parameters), empty result for an unassigned PM, 9 manual-ID cases, and no `utcnow()` left in the two route files. Suites still green: 71 (step 1), 24 (overtime), 38 (work-type IDs). Not run against MySQL or a browser | + +### Set 28 — Dashboard Showed a PM Other Projects' QR Codes (Sept 18, 2026) +| File | Fix | +|---|---| +| `routes/dashboard.py` | `_project_manager_qr_filter()` combined the assigned projects and locations with `or_`, so a PM assigned a location saw every project's QR codes carrying that location name. Now `and_`, matching the Attendance Report (§4). The same OR appeared in `dashboard_stats_api` and `dashboard_realtime_api` (counts and recent activity) — both switched too; `or_` is no longer imported | +| — | Verified against the real models on in-memory SQLite, committed vs edited module: PM with project Alpha + location "Bldg A" went from `A1, A2, B1` (B1 belongs to project Beta) to `A1`; project-only PM unchanged (`A1, A2`); location-only PM unchanged (`A1, B1`); PM with no assignments sees nothing; admin unfiltered. The Set 23/27 suites could not be re-run (scratch scripts from that session are gone), but the change is confined to `routes/dashboard.py`, which they do not cover | +| — | Deploy: `routes/dashboard.py` only, then restart. No migration | + --- ## 21. Infrastructure & Deployment diff --git a/routes/dashboard.py b/routes/dashboard.py index d80a09b..a934f4a 100644 --- a/routes/dashboard.py +++ b/routes/dashboard.py @@ -8,7 +8,7 @@ Routes: /dashboard, /project//qr-codes, /dashboard/search, """ from flask import abort, Blueprint, render_template, request, redirect, flash, session, jsonify, url_for from datetime import datetime, timedelta, date, time -from sqlalchemy import or_ +from sqlalchemy import and_ from extensions import db, logger_handler from models.attendance import AttendanceData @@ -29,6 +29,11 @@ def _project_manager_qr_filter(): if not is_pm: return False, None + # AND, not OR — the same rule the Attendance Report applies + # (_attendance_filter_conditions in routes/attendance.py): the assigned + # projects, narrowed to the assigned locations when there are any. + # With OR, a PM assigned location "Bldg A" also saw every OTHER project's + # QR codes that happened to use that same location name (Set 28). scope = [] if allowed_project_ids: scope.append(QRCode.project_id.in_(allowed_project_ids)) @@ -36,7 +41,7 @@ def _project_manager_qr_filter(): scope.append(QRCode.location.in_(allowed_location_names)) if not scope: return True, QRCode.id.is_(None) # assigned nothing → sees nothing - return True, or_(*scope) + return True, and_(*scope) bp = Blueprint('dashboard', __name__) @@ -220,7 +225,7 @@ def dashboard_stats_api(): project_base_query = project_base_query.filter(Project.id.is_(None)) if allowed_location_names: attendance_scope.append(AttendanceData.location_name.in_(allowed_location_names)) - attendance_condition = or_(*attendance_scope) if attendance_scope else AttendanceData.id.is_(None) + attendance_condition = and_(*attendance_scope) if attendance_scope else AttendanceData.id.is_(None) attendance_base_query = attendance_base_query.filter(attendance_condition) location_base_query = location_base_query.filter(attendance_condition) @@ -298,8 +303,8 @@ def dashboard_realtime_api(): )) if allowed_location_names: recent_scope.append(AttendanceData.location_name.in_(allowed_location_names)) - recent_query = recent_query.filter(or_(*recent_scope) if recent_scope - else AttendanceData.id.is_(None)) + recent_query = recent_query.filter(and_(*recent_scope) if recent_scope + else AttendanceData.id.is_(None)) recent_activity = recent_query.order_by( AttendanceData.check_in_date.desc(),