Skip to content

[Feature]: Visitor & Gate Pass management with host approval and open-visit reconciliation #285

Description

@MOHITKOURAV01

Problem Statement

Campus access is run entirely on a paper register at the gate, and it has a specific, serious failure: the school cannot answer "who is on campus right now".

That question is only ever asked during a fire drill or a real emergency, which is exactly when a paper register two buildings away is useless. In practice the register also fails in ordinary ways:

  • Visitors sign in and leave without signing out, so the register shows people on site who went home hours ago. After a week the "currently inside" count is fiction.
  • There is no host approval. Anyone can write a teacher's name in the Person to meet column and walk in; the teacher finds out when the visitor arrives at their classroom.
  • Students are released to whoever turns up. A parent collecting a child mid-day signs the same register. There is no check that the person collecting is authorised to collect that child. This is the one that should not wait.
  • Nothing is searchable. "Has this person been here before" needs somebody to leaf through a book.

Proposed Solution

A gate module built on one model covering both directions of movement, because they are the same object — a person who is inside and needs to be accounted for.

backend/models/VisitorPass.js

  • passTypevisitor (an outsider coming in) or gate-pass (a student going out)
  • Visitor identity: name, phone, email, organisation, ID proof type and a masked ID number, vehicle number, number of accompanying people
  • host / hostName — the staff member being visited, for visitor passes
  • student / studentName / className and guardianName / guardianRelation — for gate-pass
  • purpose, expectedDuration, status, approvalStatus, checkInAt, checkOutAt, badgeNumber, securityNotes
  • movements[] — an append-only audit trail of every state change with actor and timestamp

The four things this has to get right

1. "Currently on campus" must be a query, not a guess. A partial index on status: 'checked-in' makes the live roll a single indexed lookup, so the evacuation list is instant and always current.

2. The same person cannot be checked in twice. A unique partial index over (idNumber, status: 'checked-in') makes a duplicate open pass impossible at the database level rather than by convention. Doing this in application code means it holds only as long as every future code path remembers to check.

3. Check-out must be atomic and cannot precede check-in. Check-out is a conditional update filtered on status: 'checked-in'; a double-tap on the gate tablet matches nothing the second time and returns 409 rather than overwriting a check-out time and corrupting the duration. A pass that was never checked in cannot be checked out at all.

4. A student is released only to an authorised guardian. A gate-pass requires an explicit staff approval before check-out is permitted, recording who approved it. No approval, no release — enforced in the handler, not in the UI.

Overstay reconciliation

Every pass carries expectedDuration. A pass still open past its expected departure is flagged overstayed in query results, and the module exposes a bulk reconciliation endpoint that force-closes stale passes at end of day with an explicit auto-closed marker — so the register is honest about the difference between "signed out" and "we assumed they left". That distinction is the thing the paper register loses.

API — /api/visitors

Method Path Who
POST /passes admin / staff — register a visitor or gate pass
GET /passes admin / staff — filters + overstayed
GET /passes/on-campus admin / staff — the live roll
GET /passes/:id admin / staff / the host
PATCH /passes/:id/approve the named host, or admin
PATCH /passes/:id/check-in admin / staff
PATCH /passes/:id/check-out admin / staff — atomic
PATCH /passes/:id/cancel admin / staff
POST /passes/reconcile admin — bulk close stale passes
GET /my-approvals teacher — visitors waiting on me
GET /stats admin / staff

Privacy

ID numbers are stored masked (last four characters only). A gate register does not need to retain a full government ID number, and one that does is a liability the school does not need to carry.

Frontend

A /visitors page. Security and office staff get the gate desk — register, approve, check in/out, and the live on-campus roll with overstays highlighted. Teachers see only the visitors waiting on their approval.

Alternative Approaches

Separate models for visitors and gate passes. They share the check-in/check-out/audit machinery entirely; splitting them means writing the atomic check-out twice and having two answers to "who is on campus".

Skip host approval. That is the current situation.

Store full ID numbers for a stronger audit trail. The marginal audit value does not justify holding a database of government ID numbers behind a school gate terminal.

Affected Area

  • New page
  • New component
  • Authentication (host-scoped approvals)
  • Other: new Mongoose model, controller and route module

Mockups / Additional Context

The database-level uniqueness on open passes matters more than it looks: it is the difference between an invariant and a habit.

I'd like to pick this one up.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions