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, 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. 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).
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.
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 already on the ledger are
skipped exactly.
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).
