Never drop an enrolled class from upcoming lessons; delete a guardian's children with them; pin the panel's line spacing
CI / Tests (PHP 8.2) (pull_request) Successful in 58s
CI / PHPStan (pull_request) Successful in 2m53s
CI / Coding Standards (pull_request) Successful in 2m58s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m47s
CI / Build Plugin Zip (pull_request) Skipped
CI / Tests (PHP 8.1) (pull_request) Successful in 1m1s
CI / No Debug Code (pull_request) Successful in 3s
CI / Tests (PHP 8.2) (pull_request) Successful in 58s
CI / PHPStan (pull_request) Successful in 2m53s
CI / Coding Standards (pull_request) Successful in 2m58s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m47s
CI / Build Plugin Zip (pull_request) Skipped
CI / Tests (PHP 8.1) (pull_request) Successful in 1m1s
CI / No Debug Code (pull_request) Successful in 3s
Three fixes from testing the branch. A group class was only listed when its schedule resolved to exact datetimes, which needs a class time *and* a duration — both optional on the offering form, and the schedule note exists precisely so a studio can write "Tuesdays 4:00pm" instead. A class configured that way vanished from the list, which is the one thing this feature must never do. So Offering::sessionStarts() splits "when does it meet" from "how long does it run" (sessionWindows() is that plus the duration, unchanged), and SessionSchedule degrades instead of disappearing: dated rows with an open end when there is no duration, and a single row carrying Offering::scheduleLabel() when there is no time to derive dates from. Only a class whose last day has passed drops out. Deleting a guardian now deletes the children linked to them, releasing each one's lessons and enrolments first. A child account is login-less and exists only so the guardian has somebody to book for; without the guardian nobody can reach it, book for it, or be billed for it, so it was left stranded on the roster still holding slots. A `handled` set makes the re-entrant delete_user each child deletion fires a no-op, and stops a circular link recursing. The upcoming panel never stated its own line-height, so a theme setting line-height: 0 above it — the usual icon-font reset — was inherited straight through. Below 1 that produces both reported symptoms at once: stacked lines overlap, and the status pill's background is shorter than the text in it. Pinned at the same id-level specificity as the rest. Tests: composer test (863), composer lint, composer cs all pass. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+78
-21
@@ -175,22 +175,19 @@ class Offering {
|
||||
}
|
||||
|
||||
/**
|
||||
* The concrete start/end datetimes of every session of this group class,
|
||||
* derived from the class date(s), the class time, and the duration. A weekly
|
||||
* class yields one window per week from `term_start` through `term_end`; a
|
||||
* one-off class yields a single window. Returns an empty list unless the
|
||||
* schedule is fully specified (date, time, and a positive duration), so it can
|
||||
* never fabricate a session window from partial data.
|
||||
* The datetime each session of this group class starts, derived from the class
|
||||
* date(s) and the class time. A weekly class yields one per week from
|
||||
* `term_start` through `term_end`; a one-off class yields a single one.
|
||||
*
|
||||
* @return list<array{start: string, end: string}>
|
||||
* Deliberately does **not** need a duration: knowing *when* a class meets is a
|
||||
* separate question from knowing how long it runs, and a studio can quite
|
||||
* reasonably set the first without the second. Returns an empty list when
|
||||
* there is no date or no time, since neither can be invented.
|
||||
*
|
||||
* @return list<string> `Y-m-d H:i:s` starts, earliest first.
|
||||
*/
|
||||
public function sessionWindows(): array {
|
||||
if (
|
||||
null === $this->termStart
|
||||
|| null === $this->classTime
|
||||
|| null === $this->durationMinutes
|
||||
|| $this->durationMinutes <= 0
|
||||
) {
|
||||
public function sessionStarts(): array {
|
||||
if ( null === $this->termStart || null === $this->classTime ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -200,25 +197,85 @@ class Offering {
|
||||
}
|
||||
|
||||
$lastDay = null !== $this->termEnd ? $this->termEnd : $this->termStart;
|
||||
$step = new \DateInterval( 'PT' . $this->durationMinutes . 'M' );
|
||||
|
||||
$windows = [];
|
||||
$starts = [];
|
||||
$cursor = $first;
|
||||
$cursorDay = $cursor->format( 'Y-m-d' );
|
||||
|
||||
// Cap the walk at ten years of weeks so a term_end before term_start (or a
|
||||
// bad value) can never spin into an unbounded loop.
|
||||
for ( $i = 0; $i < 520 && $cursorDay <= $lastDay; $i++ ) {
|
||||
$windows[] = [
|
||||
'start' => $cursor->format( 'Y-m-d H:i:s' ),
|
||||
'end' => $cursor->add( $step )->format( 'Y-m-d H:i:s' ),
|
||||
];
|
||||
$starts[] = $cursor->format( 'Y-m-d H:i:s' );
|
||||
|
||||
$cursor = $cursor->modify( '+7 days' );
|
||||
$cursorDay = $cursor->format( 'Y-m-d' );
|
||||
}
|
||||
|
||||
return $windows;
|
||||
return $starts;
|
||||
}
|
||||
|
||||
/**
|
||||
* The concrete start/end datetimes of every session of this group class:
|
||||
* {@see sessionStarts()} closed off with the class duration. Returns an empty
|
||||
* list unless the schedule is fully specified (date, time, *and* a positive
|
||||
* duration), so it can never fabricate a session window from partial data —
|
||||
* callers that block availability or bill per session need both ends.
|
||||
*
|
||||
* @return list<array{start: string, end: string}>
|
||||
*/
|
||||
public function sessionWindows(): array {
|
||||
if ( null === $this->durationMinutes || $this->durationMinutes <= 0 ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$step = new \DateInterval( 'PT' . $this->durationMinutes . 'M' );
|
||||
|
||||
return array_map(
|
||||
static fn( string $start ): array => [
|
||||
'start' => $start,
|
||||
'end' => ( new \DateTimeImmutable( $start ) )->add( $step )->format( 'Y-m-d H:i:s' ),
|
||||
],
|
||||
$this->sessionStarts()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The last day this class meets, or null when it has no dates at all.
|
||||
*/
|
||||
public function lastClassDay(): ?string {
|
||||
return $this->termEnd ?? $this->termStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plain-language wording for when this class meets, for the places that have
|
||||
* to say something about a class whose schedule cannot be resolved to dates.
|
||||
* Prefers the studio's own note ("Tuesdays 4:00pm") — that field exists
|
||||
* precisely so a class can describe its schedule without pinning it to a
|
||||
* time — then the term dates, and finally an honest admission that nothing
|
||||
* has been set.
|
||||
*/
|
||||
public function scheduleLabel(): string {
|
||||
$note = null !== $this->scheduleNote ? trim( $this->scheduleNote ) : '';
|
||||
if ( '' !== $note ) {
|
||||
return $note;
|
||||
}
|
||||
|
||||
if ( null === $this->termStart ) {
|
||||
return __( 'Schedule to be confirmed', 'unsupervised-schedular' );
|
||||
}
|
||||
|
||||
$start = (string) mysql2date( 'M j, Y', $this->termStart );
|
||||
|
||||
if ( null === $this->termEnd || $this->termEnd === $this->termStart ) {
|
||||
return $start;
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
/* translators: 1: first class date, 2: last class date. */
|
||||
__( '%1$s – %2$s', 'unsupervised-schedular' ),
|
||||
$start,
|
||||
(string) mysql2date( 'M j, Y', $this->termEnd )
|
||||
);
|
||||
}
|
||||
|
||||
public static function fromRow( \stdClass $row ): self {
|
||||
|
||||
Reference in New Issue
Block a user