Skip to main content
Tutoring lives in the Contacts satellite at /{wsId}/users/tutoring. It turns two existing signals — attendance deficits and flagged group feedback — into scheduled make-up sessions, then tracks attendance and exports the result for payroll.

Surfaces

  • Schedule lists every session for a date-range preset (upcoming, today, tomorrow, next 7 days, this month, past, all) and can be narrowed by status, group, learner, teacher, and reason. Filters drive both the table and the export. Forward-looking presets request sortOrder=asc so the nearest session is first; past and all keep the API default of newest-first.
  • Needs support is the queue of learners with an absence deficit within the configured recent attendance window (ABSENT_RECOVERY), a require_attention feedback (WEAK_SUPPORT), or both. Scheduling from a queue row fills the learner, group, reason, and suggested times for the missing sessions. Staff choose the tutoring teacher separately. A pending recovery session reserves one absence, so staff do not schedule the same make-up twice. Feedback already linked to a pending support session does not create another scheduling suggestion. It can still appear as an office-review-only row when its lesson content has remained unchanged for the configured interval. After a completed support session, still-flagged feedback returns for reassessment after the configured interval. Older unscheduled feedback shows a parent follow-up prompt. If flagged feedback keeps the same content across the configured review interval, the queue shows an office-review alert and count. The alert compares consecutive feedback entries for that learner and group; edits made in place to an existing entry have no separate timestamp.
  • Settings stores a workspace tutoring policy with standard and English center presets. Each section starts as a read-only summary; Edit section opens only that section with Save and Cancel. Cancel discards the section draft, and other sections stay read-only while it is open. Saving merges only that section’s fields into the latest loaded policy, preserving refreshed values in other sections. Applying a preset intentionally replaces the full draft. Save, Cancel, and editor inputs are disabled during a pending save; a failed save retains the draft for retry. Operators can adjust session length, lead time, support session count, absence lookback, reassessment and follow-up intervals, scheduling horizon, class-time exceptions, named shifts, scoped group-name exclusion rules, the unchanged-content review interval, group-to-campus labels, and the parent message template. Only members with manage_workspace_settings can save it. The English center preset excludes kindergarten group names containing DDT, Mẫu giáo, or Kindergarten from all tutoring suggestions. Existing policies using the earlier unmodified DDT preset inherit these rules; custom rules remain as saved. Staff can add other naming patterns used by their workspace. Rule scope (all, make-up, or weak support), name match type, and text are editable; an interval of 0 disables office-review alerts. Older saved English center policies inherit these defaults until staff customize them.
  • Headline counts run as bounded count: exact queries, so they stay correct on workspaces with more sessions than one page. The schedule loads first; counts then load behind it. The support queue is requested when its tab is opened, and the client reuses recent results for two minutes.
Row actions mark a session done, no-show, cancelled, or back to pending, and generate the parent-facing message for the session. Staff can edit each session’s lesson content after creation, so teaching notes from the separate content sheet can be kept with the attendance record. Managers can permanently delete a pending or cancelled session after an in-app confirmation. Completed and no-show sessions stay intact for attendance and payroll history.

Enablement

Tutoring is gated by the ENABLE_TUTORING workspace config: A disabled workspace renders an enable screen rather than a 404. Members with manage_workspace_settings get an Enable tutoring button that writes ENABLE_TUTORING=true through PUT /api/v1/workspaces/{wsId}/settings/ENABLE_TUTORING; everyone else is told to ask an admin. Reading the toggle only needs view_user_groups. Tutoring shares this gate with the other Contacts modules — see Contacts module availability for the full set of toggles and states.
The page previously called notFound() for personal workspaces, so the sidebar link 404’d even though the tutoring APIs normalize personal into a real workspace id. Personal workspaces now opt in explicitly instead.

Permissions

  • view_user_groups — open tutoring and read sessions, the queue, and exports.
  • update_user_groups_scores — schedule, edit, and delete eligible sessions, mark attendance, and generate parent messages.
  • manage_workspace_settings — enable or disable the module and edit tutoring rules.
Three non-404 states replace the old blanket 404 so an operator can tell the cases apart:

Scheduling rules

One create request can carry up to 50 slots. Overlaps are rejected twice: in the form (the offending rows are highlighted before submit) and again in POST /api/v1/workspaces/{wsId}/tutoring/sessions against both the other incoming slots and the sessions already stored. A teacher conflict needs the same teacher on both slots; a learner conflict needs only the same learner. The class managers are displayed as Homeroom teacher for context. The Tutoring teacher is chosen separately for every slot from a paginated, searchable catalog of verified teacher memberships anywhere in the same center (workspace). Changing the learner’s class does not erase a selected tutoring teacher. Students and people without a teacher role are not teacher options; the create and edit APIs enforce the same workspace eligibility boundary. See teacher assignment for the API boundary. For an absence, staff enter the missed class date and select Suggest all sessions. The form scans the selected group’s upcoming scheduled classes and proposes one slot for each missing session. This naturally handles a Monday-to-Wednesday, Saturday-to-Sunday, or Sunday-to-Saturday pattern. For weak-support feedback, the English center preset proposes two consecutive 45-minute sessions before one class. Scheduling from the support queue loads these suggestions before opening the form. Staff still review the teacher, date, and time before creating any session. If too few scheduled classes are available, remaining slots stay blank for manual scheduling. The standard preset places a 45-minute session immediately before class. The Easy Center preset includes the seven named weekday and weekend shifts from the center’s schedule. The Monday/Wednesday/Friday 18:00 class uses Wednesday and Friday 17:15–18:00 only: Monday is skipped, even when it is the next scheduled class. The Tuesday/Thursday class uses 17:15–18:00. The weekend 08:00–09:30 class uses 09:30–10:30 after class, an intentional 60-minute exception to the usual 45 minutes. Other weekend shifts keep the table below. Untouched older Easy Center preset shift rules adopt these corrections when read; staff-edited rules and the standard/custom policies keep their saved behavior, including generic before-class fallback. The canonical Easy Center preset never falls back to an unlisted class day or time. Regression coverage: tutoring-policy.test.ts, tutoring-suggestion.test.ts, and the Contacts tutoring-policy-card.test.tsx, tutoring-teacher-picker.test.tsx, and tutoring-create-card.test.tsx mounted section/teacher controls. tutoring-policy-save.test.tsx covers refreshed-section isolation and pending/failed saves. The schedule displays a matching shift name alongside the actual date and time. Staff can rename, add, or change each rule. Suggestions need an existing group schedule; they never create sessions or send parent messages automatically. The parent message is a draft that staff copy after reviewing it. Campus labels are configured per class group; a clear CS1, CS2, Cơ sở 1, or Cơ sở 2 marker in the group name is inferred when no override exists. The existing generic Lớp học group tag does not identify a campus. Ambiguous groups show no campus on the schedule and Chưa xác định in the parent draft until staff set an override. The Easy Center template uses the resolved label in the parent draft. The center’s tuition remains free to learners; payroll exports count completed teaching time. When scheduling from Needs support, a make-up draft copies the recorded lesson description from the missed class date. If that class has no recorded content, the field stays empty for staff to fill. A weak-support draft uses the teacher’s flagged feedback content. Staff can review and edit either draft before saving; content is never invented from a class title or an unrelated lesson. Policies are validated at GET/PUT /api/v1/workspaces/{wsId}/tutoring/policy and stored under TUTORING_POLICY with bounded TUTORING_POLICY_PART_* rows in workspace_configs. The table enforces a 1,000-character limit per value, so the policy API writes all chunks in one upsert and readers reconstruct the validated policy. Unset or malformed values safely use the standard preset. Existing workspaces need no migration.

Exports

Detailed (per session) and payroll (per teacher, completed sessions and total minutes) exports are available as CSV and XLSX. Exports use the active filters, including the date range, and are generated server-side in pages of 1,000 rows.

Troubleshooting

Confirm the caller resolves a workspace profile at all. A missing workspace_user_linked_users row now renders the Unavailable screen; a still-hard 404 means WorkspaceWrapper could not resolve the workspace itself, which points at the app session rather than at tutoring.

The queue is empty but learners are behind

The queue reads absence deficits and user_feedbacks.require_attention. Check that group attendance is being recorded and that feedback is flagged.

The schedule or queue stays empty after loading

An API failure now shows a retry button instead of an empty table. Check the workspace permission, then retry. If the same tutoring API works on the central web domain but returns 404 Not found through Contacts, check that the central route accepts the verified Contacts app session forwarded by the satellite proxy. Schedule and queue loading show table-shaped placeholders while data is in flight.

A session cannot be created

The create button stays disabled until the group, learner, and every slot (date, time, 1–480 minutes, teacher) are complete and no two slots overlap.

Tutoring teacher assignment

In Contacts, a tutoring teacher is selected separately from the student’s homeroom teacher. By default, both session creation and editing accept any workspace user with an actual TEACHER membership in a group belonging to the same verified workspace. A teacher need not manage the student’s selected class. Student membership in that class, staff management permission, and scheduling conflict checks still apply. Users without a teacher role and teachers belonging only to another workspace remain ineligible. The teacher selector uses the paginated, searchable tutoring teacher catalog, not the general people directory or the currently loaded class managers. Multiple teacher memberships produce one catalog identity. This contract applies to the live Next.js API; the teacher catalog and unported tutoring mutations remain tracked Rust migration backlog, while the existing Rust session-list GET is unchanged. Regression coverage: apps/web/src/lib/tutoring/teachers.test.ts, apps/web/src/lib/tutoring/teacher-assignment-routes.test.ts, and the tutoring teacher catalog’s colocated route.test.ts cover workspace boundaries, teacher eligibility, permission denial, error handling, pagination, and session writes.

Queue handoff and missed-date defaults

In Contacts, scheduling an absence-recovery queue row selects the oldest supplied outstanding missed-class date in the editable form. This is a suggestion anchor, not a persisted link between an absence and a make-up session. Staff can change it; custom tutoring policy and sequential slot suggestions remain unchanged. Manual creation still starts with no missed date. Changing the learner or group clears that date so another learner’s absence is not carried into the new draft. A queue lookup belongs to its committed actor/workspace and current draft interaction. A newer queue selection, form edit, dialog open/close, scope change, or unmount invalidates the older handoff. Late results cannot overwrite a newer draft or reopen a closed dialog. Failed schedule reads retain the existing editable fallback; they do not create sessions automatically. Regression coverage: tutoring-handoff.test.tsx mounts the actual Contacts client and create card with a real QueryClient and synthetic transport responses. Its table/dialog presentation harness is bounded component evidence, not customer roster, production provider, or end-to-end dialog proof.

Archive intent when editing students

Contacts student edits preserve the stored archived intent when an existing archive-until date is loaded. An unrelated field edit or repeated open/save must not archive an explicitly active student. Selecting a temporary archive date explicitly still marks the student archived; clearing the date and switching to active retains the existing server contract that clears the archive-until value. This changes neither the temporary archive date policy nor enrollment history, and does not repair earlier customer records. Regression coverage exercises the actual student form, calendar, switch and mutation transport in form-archive.test.tsx.

Legacy absence credit matching

The support queue matches eligible absence dates and DONE/PENDING absent-recovery session dates chronologically within the configured lookback, independently for each group/student. A reservation consumes at most one absence on or before its session date; earlier make-ups cannot consume later absences. Remaining dates are returned with the same deficit and drive the make-up handoff. This is a fallback for records without a persisted source-attendance link. It does not prove which historical absence staff intended to cover, repair prior records, or alter custom eligibility/exclusion policy. Explicit attendance-row linkage remains separate work, including multiple sessions on the same date. Web queue regressions and the Rust queue parity tests cover the chronological boundary, ordering, same-day multiplicity and group/student isolation.

Staff-reviewed draft planning

Contacts offers compact per-session date/time and eligible teacher choices alongside the existing manual editor. Choices use the workspace policy, scheduled group classes, and a complete bounded read of workspace teacher memberships and tutoring sessions within the policy horizon (including the prior date for overnight conflicts). Date/time ranking is chronological; teacher ties are stable by ID. Staff explicitly select a teacher to apply a choice. Nothing is created until the existing Create action. The planner never fills an unsupported time with an invented 18:00 fallback. Each teacher/session catalog is capped at 20 pages of 100 records. Errors, inconsistent counts, duplicate or truncated pages prevent suggestions and expose Retry. Missing teachers and schedules have distinct feedback. Configured but unmaterialized class occurrences are shown as a schedule-review exception, not confirmed classes. The displayed horizon follows the configured policy rather than a fixed four weeks. Choices exclude recorded learner/teacher tutoring conflicts and other slots in the current draft, with half-open intervals and overnight overlap. Cancellation records are conservatively included to match the existing creation conflict check. These are no recorded tutoring conflict choices, not confirmed teacher availability: other classes, rooms, capacity and contractual availability are not modeled. Reads are not an atomic reservation. The server rechecks at Create, but concurrent creation remains a separate unresolved frontier. Existing custom policy, consent, staff edits and actor/workspace lifetimes remain authoritative. A late read never applies choices. Regression coverage: tutoring-draft-plan.test.ts, tutoring-plan-data.test.ts, tutoring-draft-choices.test.tsx, and the actual queue handoff regression. Source tests are not customer runtime or production-data repair evidence. Original absence linkage and automatic planning/teacher notices remain outside this packet. The next teacher-hours foundation is intended to support staff-defined defaults and explicit per-teacher overrides, with the teacher override taking precedence. Those hours are not yet implemented or consulted by this first draft packet; absence of a recorded tutoring conflict must never be treated as availability consent.

Teacher-hours domain contract

The pure @tuturuuu/utils/tutoring-teacher-hours model defines staff defaults and per-teacher weekday overrides; it does not yet read or write hours, change Contacts UI, constrain Create, or send notices. Existing custom tutoring policy and staff confirmation remain unchanged. Calendar’s automatically created 07:00–23:00 defaults are not teacher-hours authority. An absent staff weekday is unknown; an explicit empty array is closed. An absent override weekday inherits the current default. Custom teacher windows replace that weekday, rather than widening it by union. Override reset retains an empty, revisioned row; future storage must atomically reject stale revisions, including absent→create→reset races. The model preserves opaque revision strings without numeric/date conversion and rejects unchanged write receipts, but does not itself prove database CAS, role admission or same-content revision advancement. Hours require an explicitly confirmed IANA timezone. Overrides cannot establish availability without that staff frame. Windows are sorted, disjoint, half-open civil times; midnight endings use 24:00. Overnight windows must be explicitly split across weekdays. Candidate inputs are in the confirmed staff frame, never silently in the browser/class timezone. Gap/fold endpoints and slots crossing DST transitions are conservatively rejected rather than assigned an offset. Unknown coverage stays unknown; any known closed portion denies coverage. Regression coverage in tutoring-teacher-hours.test.ts keeps unconfirmed teacher hours unknown and exercises Temporal compatible gap conversion as an unsuitable input, inheritance/replacement, lossless reset receipts, adjacency, midnight, weekly wrapping and DST. The actual Calendar default factory is tested separately in Calendar’s hour-settings-shared.test.ts; utils tests stay within their package source boundary. This is pure source evidence, not persisted availability or production scheduling proof. API/schema/authenticated editor, atomic revision storage, staff-default/teacher-role permissions, outside-hours exceptions and planner consumption remain separate reviewed packets.

Private teacher-hours storage foundation

The additive storage foundation keeps confirmed staff defaults and teacher weekday overrides in private tables behind service-only, scoped RPCs. It remains inert: no current API, settings UI or Create action consumes these tables. Reads never initialize defaults. Account actors require explicit workspace MEMBER status plus view_user_groups for reads or manage_workspace_settings for writes; teacher IDs are separate workspace-user identities. Current same-workspace TEACHER membership is checked and locked for override writes. This proves current admission, not a historical role remove/readd epoch. All writes lock the workspace hours anchor, default and override in that order. Default and override revisions are positive bigint values exposed only as full-precision decimal text. Absent creation requires a null revision; updates, same-content saves and resets advance the revision. Override writes also compare the captured default revision. Reset stores a revisioned empty override instead of deleting it, so old absent/create or pre-reset drafts cannot silently revive. Unknown, explicitly closed and inherited weekdays retain the pure-model contract. The SQL window validator also rejects extra untyped interval properties. Revision-only audit receipts use canonical audit.to_record_id table/key identity and explicit account actor, without copying hours content. Audit insertion and hours mutation share one transaction; an audit failure rolls the write back. A transport timeout is still an unknown receipt, not proof of rollback: future clients must refetch and review rather than blindly replay a stale revision. No cross-request idempotency or automatic provider operation is promised. tutoring-teacher-hours-contract.yaml applies the complete schema in an owned CI disposable database, runs strict pgTAP and actual two-connection create, default-change and save/reset races, then generates an exact-schema type artifact. Node wiring tests and pure model tests are source checks; only successful exact CI supplies database evidence. The foundation requires that proof before merge. Outside-hours exceptions, API/Rust migration tracking, authenticated editing, teacher-hour consumer integration and atomic tutoring reservations remain separate reviewed work. Existing tutoring policy, workspace timezone and staff Create permissions are unchanged.

Selected-absence identity foundation (unwired)

The additive foundation is service-only and has no Contacts/API activation. Existing historical sessions remain unlinked and use the chronological fallback above; no migration guesses which absence an old make-up covered. The selected date in the current form is still suggestion context, not proof of saved attribution. The new contract identifies an absence by its attendance UUID, including separate class occurrences on the same date. Each linked make-up slot requires a distinct eligible source UUID. PENDING reserves one source; DONE permanently credits it. Cancel, no-show, or a pending reason change releases the reservation while retaining its original identity and history. Reactivation uses revision CAS and must reclaim the same eligible source; another reservation causes a conflict. Allowed deletion retains attribution; DONE credit cannot be released by deletion or a reason/status edit. Ordinary content and parent-message metadata edits preserve credit identity. Linked date/time/duration/teacher and creation actor/timestamp rewrites require an exact canonical permit. Direct schedule edits are rejected until a future validated PATCH operation exists, including moving a make-up before its source absence. No client is wired to these operations in this foundation packet. Correcting active source attendance to PRESENT requires an explicit staff-managed resolution. Silent correction or standalone deletion is rejected. A credited source keeps its history after explicit resolution. Existing workspace/group/student or membership teardown uses narrowly scoped FK cleanup; immutable original IDs/date remain available for retained records. Deleting an actual class occurrence releases pending reservations while preserving DONE credit and immutable occurrence snapshots. Declared teacher/creator SET NULL and parent rekey cascades may change only their live FK fields after the old parent disappears; arbitrary direct rewrites stay denied. Tenant-authorized release is possible after learner archive or enrollment departure; new reservations still require an active enrolled learner. Input-bound replay returns its original receipt before mutable learner/source checks, but only after rechecking the current actor’s permission. Revoked actors cannot replay. No generic trigger-depth or caller-writable session flag authorizes a protected write. Only a verified backend actor may call the canonical service-only operations, which also check current MEMBER membership and the existing tutoring permission. Private, ungranted permits exist only during an exact transaction/session/revision/mutation; success removes them and failure rolls them back. Linked status/reason/delete writes outside that boundary are rejected. Full historical schema replay, strict pgTAP, two-connection claim-versus-correction/replay races, and generated types must pass on the exact head before foundation merge. Fixtures and source checks are not live customer proof. Queue/API/UI integration and selected-card-to-transport regression are separate dependent packets; historical customer data is not repaired here.

Create completion ownership

Contacts captures the exact actor/workspace lifetime and draft revision before a make-up create request. Only that still-current intent may close its draft, announce success/failure, or invalidate its scoped queries. Replacing the draft or leaving and returning to an actor/workspace permanently invalidates old completion effects. A new scope remains usable while an old admitted write is pending. This does not cancel or undo a server write: current-scope success and failure remain visible, and synchronous duplicate submission is suppressed. The mounted Client/CreateCard regressions in tutoring-create-scope.test.tsx cover these boundaries with held synthetic transports; they are not customer or production-write evidence. The inert absence identity contract checks rejected linked-session mutations against the canonical-operation error and preserved schedule and attribution. Trigger permit lookups use distinct local variable names and qualified columns to avoid ambiguous SQL references before admission. Regression evidence lives in apps/database/supabase/tests/tutoring-absence-credit-contract.sql and apps/database/supabase/tests/tutoring-absence-credit-lifecycle.sql; the exact-head database contract must pass before this foundation is considered verified.