feat(booking): persist service duration and allow full service replacement
Appointment gains:
- replaceServiceItems(): full replacement that unconditionally syncs the
legacy single serviceItem column. addServiceItem() only fills it when null,
which would leave stale service names in the four consumers that read
service_item (admin lists, public site, desktop app).
- currentServiceUuids(): input-order uuids, falling back to the single column
for appointments created before multi-service support.
- service_total_minutes / service_buffer_minutes (both nullable, NULL in slot
mode). slot_end - slot_start carries the number but cannot say whether it
was intentional, and a reserve entry has slot_start == slot_end so its
duration had nowhere to live.
Existing columns untouched: slot_start, slot_end, active_slot_key, is_reserve
verified unchanged via SHOW COLUMNS.
Task: docs/new_feture/taskes/task-00-service-mode-completion/
Slot-mode contract: unchanged (--group=slot-mode-frozen green)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -206,6 +206,25 @@ class Appointment
|
||||
#[ORM\Column(name: 'is_reserve', type: 'boolean', options: ['default' => false])]
|
||||
private bool $isReserve = false;
|
||||
|
||||
/**
|
||||
* مدتِ محاسبهشدهٔ ترکیب سرویسها در لحظهٔ ثبت — فقط در حالت نوبتدهی سرویسی.
|
||||
*
|
||||
* `slot_end - slot_start` همین عدد را دارد ولی نمیگوید عمدی بود یا دستی؛ و برای نوبت
|
||||
* رزرو (که `slot_start == slot_end` است) هیچجای دیگری مدت نگهداشته نمیشود، پس تبدیل
|
||||
* رزرو به نوبت زماندار بدون این ستون مدت را از دست میدهد.
|
||||
*
|
||||
* در حالت اسلاتی همیشه NULL میماند.
|
||||
*/
|
||||
#[ORM\Column(name: 'service_total_minutes', type: 'smallint', nullable: true)]
|
||||
private ?int $serviceTotalMinutes = null;
|
||||
|
||||
/**
|
||||
* `buffer_minutes` مؤثر در لحظهٔ ثبت. تغییر بافر در تنظیمات نباید معنای نوبتهای
|
||||
* ثبتشده را عوض کند (قانون پنجم مستند: هر چیزی که ثبت شد همانطور میماند).
|
||||
*/
|
||||
#[ORM\Column(name: 'service_buffer_minutes', type: 'smallint', nullable: true)]
|
||||
private ?int $serviceBufferMinutes = null;
|
||||
|
||||
#[ORM\Column(name: 'created_at', type: 'integer')]
|
||||
private int $createdAt;
|
||||
|
||||
@@ -289,6 +308,51 @@ class Appointment
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* جایگزینی کامل سرویسهای نوبت (برای ویرایش و جابهجایی سرویسآگاه).
|
||||
*
|
||||
* برخلاف {@see addServiceItem()}، ستون تکیِ `serviceItem` را **بیقید** با اولین عضو
|
||||
* همگام میکند: چهار مصرفکننده روی `service_item` تکی خواندهاند (`AppointmentsPage`،
|
||||
* `ReserveAppointmentsPage`، `nobat724_front/services/response.js`،
|
||||
* `clinic-pro-tauri/src/service/response.js`) و رهاکردنش یعنی نوبت با سرویسهای جدید
|
||||
* ولی نامِ سرویس قدیمی در فهرستها. همان الگوی `ServiceItem::setStaffMembers()`.
|
||||
*
|
||||
* @param \App\ClinicService\Entity\ServiceItem[] $items
|
||||
*/
|
||||
public function replaceServiceItems(array $items): self
|
||||
{
|
||||
$this->serviceItems->clear();
|
||||
foreach ($items as $item) {
|
||||
if (!$this->serviceItems->contains($item)) {
|
||||
$this->serviceItems->add($item);
|
||||
}
|
||||
}
|
||||
$this->serviceItem = $items[0] ?? null;
|
||||
$this->updatedAt = time();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* uuid سرویسهای فعلی، به ترتیب. نوبتهای پیش از چند-سرویسیشدن فقط `serviceItem`
|
||||
* تکی دارند، پس آن هم پوشش داده میشود.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function currentServiceUuids(): array
|
||||
{
|
||||
$uuids = array_map(
|
||||
fn(\App\ClinicService\Entity\ServiceItem $i) => $i->getUuid(),
|
||||
$this->serviceItems->toArray(),
|
||||
);
|
||||
|
||||
if ($uuids === [] && $this->serviceItem !== null) {
|
||||
$uuids = [$this->serviceItem->getUuid()];
|
||||
}
|
||||
|
||||
return array_values($uuids);
|
||||
}
|
||||
public function getStaff(): ?\App\Staff\Entity\ClinicStaff { return $this->staff; }
|
||||
public function isDepositRequired(): bool { return $this->depositRequired; }
|
||||
public function getDepositAmountRials(): ?int { return $this->depositAmountRials; }
|
||||
@@ -297,6 +361,18 @@ class Appointment
|
||||
public function getInsuranceBaseId(): ?int { return $this->insuranceBaseId; }
|
||||
public function getInsuranceSupplementaryId(): ?int { return $this->insuranceSupplementaryId; }
|
||||
public function isReserve(): bool { return $this->isReserve; }
|
||||
public function getServiceTotalMinutes(): ?int { return $this->serviceTotalMinutes; }
|
||||
public function getServiceBufferMinutes(): ?int { return $this->serviceBufferMinutes; }
|
||||
|
||||
/** هر دو با هم ست میشوند: مدت بیبافر و بافر بیمدت هیچکدام معنا ندارند. */
|
||||
public function setServiceDuration(?int $totalMinutes, ?int $bufferMinutes): self
|
||||
{
|
||||
$this->serviceTotalMinutes = $totalMinutes;
|
||||
$this->serviceBufferMinutes = $totalMinutes === null ? null : $bufferMinutes;
|
||||
$this->updatedAt = time();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setServiceSection(?\App\ClinicService\Entity\ServiceSection $v): self { $this->serviceSection = $v; return $this; }
|
||||
public function setServiceItem(?\App\ClinicService\Entity\ServiceItem $v): self { $this->serviceItem = $v; return $this; }
|
||||
@@ -430,6 +506,9 @@ class Appointment
|
||||
'insurance_base_id' => $this->insuranceBaseId,
|
||||
'insurance_supplementary_id' => $this->insuranceSupplementaryId,
|
||||
'is_reserve' => $this->isReserve,
|
||||
// فقط در حالت نوبتدهی سرویسی پر میشوند؛ در حالت اسلاتی null.
|
||||
'service_total_minutes' => $this->serviceTotalMinutes,
|
||||
'service_buffer_minutes' => $this->serviceBufferMinutes,
|
||||
'version' => $this->version,
|
||||
'created_at' => $this->createdAt,
|
||||
'updated_at' => $this->updatedAt,
|
||||
|
||||
Reference in New Issue
Block a user