/{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=ascso the nearest session is first;pastandallkeep 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), arequire_attentionfeedback (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_settingscan save it. The English center preset excludes kindergarten group names containingDDT,Mẫu giáo, orKindergartenfrom all tutoring suggestions. Existing policies using the earlier unmodifiedDDTpreset 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: exactqueries, 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.
Enablement
Tutoring is gated by theENABLE_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.
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 inPOST /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
The sidebar link 404s
Confirm the caller resolves a workspace profile at all. A missingworkspace_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 anduser_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 returns404 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 actualTEACHER 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 storedarchived 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 plusview_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 intutoring-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.