Parent–Teacher Meeting (PTM) Module

How We Built a Production-Grade PTM Module Inside a Multi-School ERP SaaS — A Deep Technical Breakdown

If you've been following ProjectWorlds, you know we don't just ship feature lists — we ship complete, production-grade systems built on real engineering decisions. Today, we're going deep on one of the most requested additions to our Multi School ERP SaaS Script + Mobile App V2.0: the Parent–Teacher Meeting (PTM) Module.

This isn't a "we added a calendar event" blog post. This is a full architectural walkthrough — the data model, the multi-tenant scoping, the billing-safe notification design, the RBAC enforcement, and every decision that went into making this bulletproof for any school running on our platform.


What Is the PTM Module?

The PTM module is a full lifecycle system for managing Parent–Teacher Meetings inside your school ERP. From scheduling and guardian notification (via WhatsApp, SMS, email, and push) to recording attendance, capturing per-student teacher remarks, collecting parent feedback, managing follow-up action plans, and generating class-wise/student-wise PDF reports — everything is handled in one place.

Here's a quick snapshot of what the live dashboard looks like:

  • Meetings This Session: Count of PTMs scheduled in the current academic year
  • Upcoming Meetings: Next scheduled/notified PTMs
  • Parent Attendance: Real-time percentage of guardians who showed up
  • Open Follow-Ups: Pending action items from past meetings
  • Remark Completion: What percentage of invited students have teacher remarks logged

All of this is available for admins and teachers (with appropriate access scoping), and a simplified parent-facing view is served through the existing Parent Portal without any extra login or app.


Why Build It This Way?

Before writing a single line, we audited every requested feature against what already existed in the V2.0 codebase. The result was striking:

~60% of the "new" features were already solved platform problems.

Feature Status Reused Infrastructure
WhatsApp notification ✅ Already solved WhatsAppChannel + Gupshup/Meta/360Dialog gateways + comms wallet billing
SMS alerts ✅ Already solved SmsChannel + DLT template binding + wallet debit/refund
PDF reports ✅ Pattern exists HandlesPdfAssets trait + dompdf, same as marksheets/certificates
Parent portal view ✅ Already exists routes/parent.php, multi-child switch, parent layout
Class-wise dashboards ✅ Pattern exists ExamDashboardController graceful-degradation style
Parent attendance tracking 🆕 New
Teacher & parent remarks 🆕 New (mirrors behavior records)
Follow-up management 🆕 New (mirrors enquiry follow-ups)

The real work was 5 new database tables + scheduling/RBAC logic + one new notification type, all wired into infrastructure that already handles wallet billing, DLT compliance, deliverability logging, and multi-gateway fallback.

Building a bespoke Gupshup integration inside the PTM module would have been a mistake — it would have bypassed all of that. We didn't.


The Data Model — 5 Tables, Designed to Last

ptm_meetings — The Scheduled Event

The top-level entity. Every PTM starts here.

id, school_id, academic_session_id,
title, description,
scope ENUM('school', 'class', 'section'),
school_class_id (nullable), section_id (nullable),
meeting_date, start_time, end_time,
mode ENUM('in_person', 'online', 'hybrid'),
venue (nullable), online_link (nullable),
status ENUM('draft','scheduled','notified','in_progress','completed','cancelled'),
created_by (staff_id)

Two design decisions worth calling out:

  • The scope enum lets one PTM cover a whole school, a single grade, or one section. No schema change needed when a school wants a "whole-school PTM" versus a class-specific one.
  • The mode field future-proofs online/hybrid meetings (Zoom links, Google Meet, etc.) from day one.
  • The status lifecycle is enforced in code — you can't take attendance on a draft meeting, and you can't send notifications on a cancelled one.

ptm_invitations — The Guardian/Student Join + Attendance

One row per student invited to a PTM. This is where attendance lives.

id, school_id, ptm_meeting_id, student_id,
guardian_phone (snapshot at invite time),
slot_time (nullable),
notified_at, notification_status,
attendance_status ENUM('pending','present','absent','rescheduled'),
checked_in_at, recorded_by (staff_id)
unique(ptm_meeting_id, student_id)

Separating invitation/attendance from the meeting itself gives us clean class-wise and student-wise attendance reports. The slot_time column is already there for future appointment-slot booking UI — zero schema change needed.

The guardian_phone is snapshotted at invite time — if a parent updates their phone number later, historical notification records stay accurate.

ptm_remarks — Teacher's Per-Student Notes

Mirrors the StudentBehaviorRecord pattern already in the codebase.

id, school_id, ptm_meeting_id, ptm_invitation_id,
student_id, staff_id, academic_session_id,
performance_remark (text),
attendance_remark (text),
behaviour_remark (text),
overall_rating (tinyint 1-5, nullable),
visible_to_parent (bool, default true)

The overall_rating alongside free-text remarks means reports can aggregate trends across PTMs and sessions — not just store notes that nobody reads again.

The visible_to_parent flag gives teachers control over which remarks surface in the parent portal. Sensitive staff notes can be kept internal.

ptm_feedback — Parent's Feedback

A completely separate table from remarks — this is parent-authored.

id, school_id, ptm_meeting_id, student_id,
submitted_by_user_id,
rating (tinyint 1-5, nullable),
comments (text)

Keeping feedback and remarks in separate tables means either side can evolve independently — multi-question feedback surveys, rubric-based remarks — without coupling changes to both.

ptm_followups — Action Plans

Mirrors the EnquiryFollowUp pattern.

id, school_id, ptm_meeting_id, student_id,
assigned_to (staff_id), action_plan (text),
due_date (nullable),
status ENUM('open','in_progress','done','cancelled'),
completed_at (nullable), created_by (staff_id)

Status transitions automatically set completed_at when a follow-up moves to done.

Every mutable table implements OwenIt\Auditing\Contracts\Auditable — full change history on meetings, remarks, and follow-ups, same as Homework does in the rest of the platform.


The Multi-Tenant Architecture — Non-Negotiable Rules

Every table has school_id in $fillable and every model uses BelongsToSchool. This is the platform convention — auto-scoping is global, and raw IDs from requests are never trusted without scope. Break this rule and you have a data leak between schools in a multi-tenant SaaS.

Every table also stores academic_session_id, resolved via AcademicContext::getSessionId(). Year-over-year history and per-session reporting work out of the box.

The module is registered in config/modules.php with is_core => false — schools can enable or disable PTM without affecting anything else.


The Notification Design — Wallet-Safe, Compliant, Multi-Channel

This is where a lot of ERP scripts get it wrong. They call Gupshup directly, skip DLT template validation, ignore wallet balance, and have no deliverability logging.

We didn't do any of that.

PtmScheduledNotification implements ShouldQueue and declares channels in via() gated on each school's settings:

  • send_whatsapp && school->whatsapp_enabled → WhatsAppChannel
  • send_sms && school->sms_notifications_enabled → SmsChannel
  • send_push && school->fcm_enabled → FcmChannel
  • send_email → MailChannel with AppliesSchoolSmtp

The DLT template binding, wallet debit/refund, deliverability logging, and multi-gateway fallback (Gupshup → Meta → 360Dialog) all come for free from the channel layer.

Default templates are seeded via NotificationTemplateSeeder with variables: student_namemeeting_datemeeting_timevenueclass_nameschool_name, and more. School admins can customize templates in Settings → Notifications — the same UI they use for every other notification type.

The Hard-Block Cost Preview

Bulk-inviting a whole school can be expensive. Before a single message goes out, the system:

  1. Resolves which channels are active for the school
  2. Estimates per-channel cost for the full batch using conservative per-unit rates
  3. Checks the school's comms wallet balance via CommsWalletService
  4. Refuses to send unless the wallet covers the entire batch — no partial paid sends

The meeting's Show page surfaces a cost-preview modal with a per-channel breakdown, total estimate, current balance, and a hard-block alert with a wallet top-up link if the balance is insufficient. Only fully-affordable batches proceed. This isn't optional — it's enforced in PtmInvitationService before any notification is dispatched.


RBAC — Scoped to the Centimetre

PTM has 6 permission strings:

ptm.dashboard.view     — dashboard + guide (admin + teacher)
ptm.meeting.manage     — schedule, cancel, send invitations (admin only)
ptm.attendance.manage  — record parent attendance (teacher, own allotment)
ptm.remark.manage      — write teacher remarks (teacher, own allotment)
ptm.followup.manage    — create/close follow-ups (teacher, own allotment)
ptm.report.view        — class-wise/student-wise reports + PDF (teacher, own classes)

ptm.meeting.manage is not granted to teachers — only admins schedule meetings and trigger billable notifications. This was a deliberate decision: billing responsibility stays with school administration.

Teacher access to attendance, remarks, and follow-ups is scoped via Staff::getAllottedSectionIds() — the same helper used by Classwork and Homework. A teacher opening a meeting outside their allotment gets a clean 403. A teacher's roster view only shows students enrolled in their allotted sections.

The remark ownership guard adds a second layer: a teacher can only edit a remark row they authored (staff_id match). Admins (no staff association) can edit any remark. Other teachers' remarks show as read-only.

The platform's CheckPermission middleware was extended during this build to support OR-gating (permission:ptm.attendance.manage|ptm.remark.manage) via canAny. This is now available for all modules.


The Parent Portal

The parent-facing side lives in routes/parent.php behind parent_auth middleware and respects the existing switch-child session for multi-child families.

  • Index: Lists only PTMs the active child was invited to with status notifiedin_progress, or completed. Drafts and cancelled meetings are hidden.
  • Show: Displays meeting details, teacher remarks where visible_to_parent = true, and a star-picker feedback form.
  • Feedback: Upserts a ptm_feedback row — one editable record per parent per meeting. Parents can update their feedback; the latest version is what's reported.

A "Parent Meetings" link was added to the parent sidebar under ACADEMICS using the existing BuildMenuListener::buildParentMenu pattern.


Reports and PDF

PtmReportController generates two views — class-wise summary and student-wise detail — both scoped to the teacher's allotment.

The class-wise summary surfaces: attendance percentage, remark coverage percentage, feedback count, feedback average rating, and open follow-up count per class/section.

The student-wise detail shows: individual attendance status, overall remark rating, presence of a remark, parent feedback rating, and follow-up counts.

Both views have a PDF export route using Barryvdh\DomPDF via the HandlesPdfAssets trait — the same pattern used for marksheets and fee receipts. The generated PDFs are real (879 KB, application/pdf verified during development).


The Phased Delivery — How We Actually Shipped It

The module was delivered across 6 phases, each independently shippable as its own PR:

Phase What shipped
Phase 0 — Foundation 5 migrations, 5 models, config/modules.php entry, permission seeder
Phase 1 — Scheduling & roster Dashboard, meeting CRUD, roster generation, menu wiring
Phase 2 — Notifications PtmScheduledNotificationPtmInvitationService (cost preview + hard-block), 4 default templates, cost-preview modal
Phase 3 — Attendance & remarks PtmRecordController (combined save), teacher scoping, remark ownership guard, CheckPermission OR-gate fix
Phase 4 — Parent portal + feedback Parent index/show/storeFeedback, sidebar link, feedback upsert
Phase 5 — Follow-ups & reports PtmFollowupController (status board + AJAX roster), PtmReportController + PDF

All development and testing happened in the main checkout (not a worktree) — a requirement of the project's database test configuration.


Try It on ProjectWorlds

The PTM module is part of the ProjectWorlds Multi School ERP SaaS Script + Mobile App V2.0 — our most advanced school management platform, built on Laravel 12 with Flutter mobile apps, live GPS tracking, AI insights, and biometric integration.

👉 Explore the full platform at projectworlds.com

Whether you're a school looking for a complete ERP, or a developer who wants to study how a production multi-tenant SaaS handles real-world complexity — notifications, RBAC, billing, auditing, multi-tenancy — this codebase is a masterclass.

The PTM module alone covers:

  • 24 named routes (school + parent)
  • 5 database tables with full audit trails
  • 6 RBAC permission strings with sub-allotment scoping
  • 1 notification type with 4 channel templates, wallet-safe bulk send, and cost preview
  • PDF report generation
  • Parent portal integration with multi-child support

And it's all reusing platform infrastructure — not reinventing it.


What's Next — Phase 6 Bolt-Ons

The following are planned as optional future additions, requiring zero schema changes (the tables already support them):

  • Day-before reminder cron — SendPtmReminders command, mirrors SendFeeDueReminders, wallet-checked before dispatch
  • Per-student appointment slot booking UI — slot_time column already exists on ptm_invitations
  • Bulk remark templates — teacher selects a template, populates all students, edits individually
  • Online meeting link management — online_link already on ptm_meetings

Have questions about the architecture, the notification design, or how to extend the PTM module for your school? Drop a comment below or reach out via projectworlds.com. We're always building.

Posted in Multi School ERP and tagged , , , , , , , , , , .

Leave a Reply