feat(admin): finish the screens that were stopping one step short

Five places where the data existed and the screen did not use it.

Booking a whole course had no button because it needs a doctor and the course
does not carry one — each session can be with a different doctor. The page now
asks for the doctor the same way the resource booking page does, and the button
explains that it is all-or-nothing before it is pressed.

A course whose package does not cover the remaining sessions is still valid —
the rest is simply charged normally — but nobody was told. The course response
carries package_balance and the shortfall, and the page warns. Before session
six, not during it.

The credit ledger already returned who recorded a row and which appointment it
belonged to, and showed neither. An adjustable ledger without the name of the
person who adjusted it is half an audit trail.

Version history printed a JSON blob of each version's effects, which does not
answer the question anyone actually has: what changed? It now diffs each
version against the previous one, field by field, and says so plainly when a
version changed nothing meaningful.

A resource with no calendar showed "—" for utilization. Null means undefined,
not zero, and the next step is always the same: set up the calendar. It is a
link now. The report range also accepts a custom from/to, kept in the URL like
the rest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-01 15:07:27 +03:30
co-authored by Claude Opus 5
parent fe48b10fb5
commit 5c754244f2
10 changed files with 314 additions and 27 deletions
@@ -171,8 +171,20 @@ class TreatmentCourseController extends BaseController
usort($sessions, static fn (CourseSession $a, CourseSession $b): int
=> $a->getSessionNumber() <=> $b->getSessionNumber());
// مانده در برابر جلسات باقی‌مانده: **هشدار** است نه خطا. دوره‌ای که پکیجش کفاف
// نمی‌دهد هنوز کاملاً معتبر است — بقیه‌اش نقدی می‌شود — ولی کسی باید بداند،
// ترجیحاً قبل از جلسهٔ ششم نه سرِ آن.
$package = $course->getPatientPackage();
$balance = $package === null ? null : $this->credits->balance($package);
$needed = count(array_filter(
$sessions,
static fn (CourseSession $s): bool => $s->getStatus() !== CourseSession::STATUS_COMPLETED,
));
return $course->toArray() + [
'progress' => $this->progress->progressOf($course),
'progress' => $this->progress->progressOf($course),
'package_balance' => $balance,
'package_shortfall' => $balance === null ? null : max(0, $needed - $balance),
'sessions' => array_map(
static fn (CourseSession $s): array => $s->toArray(),
$sessions,