Create a Semester
A semester is created through a wizard: two forms, then a chain of CSV imports. Each import’s Cancel button goes to the next step, so an admin who has nothing to upload yet can walk straight through and come back later — every step can be re-run any time from Admin › Semesters.
Ready-made CSVs for every step are on the Sample Data page — useful for trying the whole flow on a fresh install.
Before the first semester: bootstrap
On a fresh install, Schedule and Calendar redirect to
/admin/setup/index.php — “Welcome! Let’s set up the portal.” — which walks
through the prerequisites:
- Locations — confirm or CSV-import the teaching sites.
- Teachers — CSV-import the teaching staff. Rows match existing people
by email (then by phone), get a
usersrow with an empty password (no login), and a teacher profile. - (Optional) Students & Parents — one CSV for the whole roster: one row per person, where a student row’s “Parents” column names or emails their parents (each must match another row or an existing person). Anyone listed as a parent becomes a parent; everyone else becomes a student; siblings naturally share parent rows.
- Create your first semester — unlocked once locations and teachers exist.
Step 1 — the semester itself
/admin/semester/new.php: season (fall / spring / summer / test), year,
start and end dates, and all the pricing — the registration fee (one per
family), tuition in two schedules (Bronx residents and non-residents /
online students: 30- and 60-minute lesson fees and the guitar ensemble fee
for each), the recital & logistics fee, installment plan fee, the second
installment due date, and lessons-per-semester (default 15, used for
per-lesson price display and duration-change math). Automatic charge-posting
currently uses the resident rates; the non-resident schedule is stored for
quoting and reference.
The second installment due date is when the remaining balance is due for families on the installment plan. Left blank, it defaults to the midpoint of the semester’s dates; it appears in the fee notices on the registration form and billing pages, and drives the “behind on payments” rule for installment-plan families (semesters without one fall back to the old half-way-lesson rule).
Duplicate season+year is rejected (“Fall 2026 already exists”). On success the new semester immediately becomes the admin’s selected semester.
Dates and pricing can be edited later (/admin/semester/edit.php) — with a
warning that changing the date range does not add or remove lessons
(lessons come from the imported class dates), and a count of class dates
that would fall outside the new range.
Step 2 — active locations
Checkbox list of which locations are in use this semester (a new semester pre-checks everything). Saving builds the import chain and sends the admin into it. Removing a location later does not clean up its dates, teacher assignments, or reservations — prune those yourself.
Steps 3–8 — the CSV imports
All six use the same four-stage pipeline: Upload (file or pasted text, comma or tab) → Mapping (each CSV column mapped to a field; close names are auto-matched, so different headers are fine) → Validation (every row shown as Valid or Error with a message; error rows are simply skipped) → Commit (“3 created, 1 updated, 2 skipped”). Re-importing is safe by design: rows that already exist are updated or skipped, never duplicated.
Step 3 — Class days (location_weekdays)
Location Name, Day, Start Time, End Time — one row per weekday each
location holds classes on, with that day’s standard hours (e.g. Bronx
Community College: Saturday 9:00 am–5:00 pm and Tuesday 3:30–8:00 pm). This
is the explicit declaration the rest of the semester hangs off: the
class-dates import rejects dates on undeclared days, rows there with blank
times inherit these hours, and the Semester Schedule draws one grid per
declared day over them. Re-importing updates hours in place.
Step 4 — Class dates (location_dates)
One row per class date per location: Location Name, Date, Start Time,
End Time, Status (active/inactive, blank = active), Notes (“Day 1”,
“Holiday Week” — shown on calendars as the date’s title). A location may meet
on more than one weekday — Saturdays at both sites and Tuesday evenings at
one, say — with each date carrying its own hours; each weekday’s track keeps
its own “Day 1…Day N” numbering, matching how lessons are numbered. Inactive
rows are breaks: no lessons are generated for them, and families see the
notes text. Dates outside the semester range are a warning, not an error.
Committing also re-syncs existing lessons and hold blocks at each touched location — confirmed reservations drop future lessons on dates that are no longer active and generate any newly active ones.
Step 5 — Location teachers (location_teachers)
Teacher Name, Location Name, Day (optional) — which teachers teach
where, and on which day of the week. These become the columns of the
Semester Schedule — the schedule draws one grid per class day, and a
teacher appears only in the grids of the days listed here; nothing can be
scheduled for a teacher at a location on a day without one. A blank Day
assigns the teacher to every weekday the location holds classes on. The
import is purely additive (existing assignments are skipped, never removed).
Assignments can also be edited one at a time from the teacher’s edit page: the Schedule Days card lists the teacher’s (location, day) pairs for the selected semester, with an “Add a day…” select offering every remaining valid pair and a Remove button per row. Removing is refused while the teacher still has reservations, hold blocks, or calendar lessons there that day — the column can’t be pulled out from under a schedule.
Step 6 — Hold blocks (hold_blocks)
Teacher Name, Location Name, Day, Start Time, End Time, Title
(“Lunch”) — a teacher’s standing non-lesson time. Each row holds that slot
on every class date this semester. End must be after start, at most 4 hours;
the teacher must already have a column at that location; and every row is
conflict-checked three ways — against earlier rows in the same file, against
existing weekly bookings, and against every future materialized date.
The CSV has no kind column, so each block’s kind is inferred from its title: “ensemble” → Guitar Ensemble, “musicianship” → Musicianship Skills, anything else → a plain hold. The kind drives the block’s color on the schedule grid and can be changed later in the block’s edit modal.
Step 7 — The schedule itself (reservations)
One row per weekly lesson slot — how an existing schedule moves into the
portal: Student Name (or email), Teacher Name, Location Name, Day,
Start Time, Duration Minutes (blank = 30), Status (pending reach out /
pending confirmation / confirmed).
- Confirmed rows generate their lessons but post no charges — balances carried over from the old system come in step 7, so nobody is billed twice.
- Rows are checked against the location’s calendar (“no active class dates on Saturdays this semester”) and five layers of conflicts, including student double-booking — “a CSV has no eyes on it,” so the importer checks what the grid leaves to the admin’s eyes.
- A row whose slot is already reserved for the same student is “Already reserved (no change)”.
Step 8 — Opening charges and payments (ledger_entries)
One row per charge families already ran up and per payment they already
made, on the dates they happened: Student Name, Entry Type
(registration / lessons / recital fee / payment / scholarship / other),
Amount (always positive), Date (blank = semester start), Debit or Credit (defaults by type; required for “other”), Description. This is
what makes the schedule grid color-code who owes what from day one.
Re-uploading the same file is a no-op — rows matching an entry already
on the ledger show “Already recorded (no change)” and are skipped. But
identical rows within one file are all kept: a student with two
same-priced lesson blocks legitimately owes the charge twice, so the second
row imports too, flagged “(same as row N — both kept)” in the preview.
The chain ends on the Semester Schedule.
The other way in: pre-populate from a previous semester
Instead of the step-6 CSV, /admin/semester/prepopulate.php copies a
previous semester’s schedule: every student keeps their teacher, location,
day, and time, and everything arrives as pending_reach_out — the list
to call through. Nothing is confirmed, no lessons are generated, and nobody
is charged until each reservation is confirmed.
A preview table shows exactly what will happen per reservation — including each one’s status last semester — before committing:
- Skipped with an error: the teacher no longer teaches at that location this semester (import location teachers first), or the slot is now taken (by another carried-forward reservation or an existing booking/hold block).
- “Already carried over (no change)”: matching is by student + teacher + location — not by slot — so re-running after times have been shuffled never creates duplicates. (Corollary: if a student was moved to a different teacher in the new semester, re-running would carry them again — delete the stale copy.)
- Deleted reservations, and reservations whose student or teacher has since been soft-deleted, don’t come across at all.
Hold blocks are not carried forward — import them per semester (step 5).
