feat(services): match Figma خدمات page in settings shell + multi-staff
Render the clinic services page (sections → services) inside the settings sub-navigation shell (SettingsLayout, "خدمات" active) to match the Figma settings design. Restyle section cards to show the service count and a status toggle with edit/delete actions, and service cards with labelled price/duration and personnel chips. A service can now have multiple personnel: add an additive many-to-many ServiceItem↔ClinicStaff (staffMembers, EAGER) while keeping the legacy single `staff` column mirrored for backward compatibility. Endpoints accept `staff_uuids[]` (falling back to the legacy single `staff_uuid`) and return `staff_members[]`; the section list now reports `items_count`. Backfill-safe: pre-migration rows fall back to the single staff in toArray. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -50,9 +50,12 @@ class ClinicServiceController extends BaseController
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$this->assertServicesGate($entityType, $entityId);
|
||||
|
||||
$sectionEntities = $this->sectionRepo->findByEntity($entityType, $entityId);
|
||||
$counts = $this->itemRepo->countBySections($sectionEntities);
|
||||
|
||||
$sections = array_map(
|
||||
fn(ServiceSection $s) => $s->toArray(),
|
||||
$this->sectionRepo->findByEntity($entityType, $entityId)
|
||||
fn(ServiceSection $s) => $s->toArray($counts[$s->getUuid()] ?? 0),
|
||||
$sectionEntities
|
||||
);
|
||||
|
||||
return $this->success($sections);
|
||||
@@ -158,12 +161,9 @@ class ClinicServiceController extends BaseController
|
||||
|
||||
$item = new ServiceItem($section, $name, (int) ($data['price_rials'] ?? 0));
|
||||
|
||||
if (!empty($data['staff_uuid'])) {
|
||||
$staff = $this->staffRepo->findByUuid($data['staff_uuid']);
|
||||
if ($staff === null || $staff->getEntityType() !== $entityType || $staff->getEntityId() !== $entityId) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'پرسنل انتخابشده متعلق به شما نیست', 422, 'staff_uuid');
|
||||
}
|
||||
$item->setStaff($staff);
|
||||
$staffError = $this->applyStaffMembers($item, $data, $entityType, $entityId);
|
||||
if ($staffError !== null) {
|
||||
return $staffError;
|
||||
}
|
||||
|
||||
if (isset($data['insurance_covered'])) {
|
||||
@@ -201,15 +201,11 @@ class ClinicServiceController extends BaseController
|
||||
if (isset($data['name']) && trim($data['name']) !== '') { $item->setName(trim($data['name'])); }
|
||||
if (isset($data['price_rials'])) { $item->setPriceRials((int) $data['price_rials']); $priceChanged = true; }
|
||||
if (isset($data['active'])) { $item->setActive((bool) $data['active']); }
|
||||
if (array_key_exists('staff_uuid', $data)) {
|
||||
$staff = null;
|
||||
if ($data['staff_uuid']) {
|
||||
$staff = $this->staffRepo->findByUuid($data['staff_uuid']);
|
||||
if ($staff === null || $staff->getEntityType() !== $entityType || $staff->getEntityId() !== $entityId) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'پرسنل انتخابشده متعلق به شما نیست', 422, 'staff_uuid');
|
||||
}
|
||||
if (array_key_exists('staff_uuids', $data) || array_key_exists('staff_uuid', $data)) {
|
||||
$staffError = $this->applyStaffMembers($item, $data, $entityType, $entityId);
|
||||
if ($staffError !== null) {
|
||||
return $staffError;
|
||||
}
|
||||
$item->setStaff($staff);
|
||||
}
|
||||
if (isset($data['insurance_covered'])) {
|
||||
$item->setInsuranceCovered((bool) $data['insurance_covered']);
|
||||
@@ -312,6 +308,34 @@ class ClinicServiceController extends BaseController
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Resolve and assign the service's personnel from the payload, scoped to the
|
||||
* tenant. Accepts `staff_uuids` (array, preferred) or the legacy single
|
||||
* `staff_uuid`. Returns a 422 JsonResponse if any staff is missing or not
|
||||
* owned by the tenant, otherwise null.
|
||||
*/
|
||||
private function applyStaffMembers(ServiceItem $item, array $data, string $entityType, int $entityId): ?JsonResponse
|
||||
{
|
||||
$uuids = [];
|
||||
if (array_key_exists('staff_uuids', $data) && is_array($data['staff_uuids'])) {
|
||||
$uuids = $data['staff_uuids'];
|
||||
} elseif (!empty($data['staff_uuid'])) {
|
||||
$uuids = [$data['staff_uuid']];
|
||||
}
|
||||
|
||||
$members = [];
|
||||
foreach (array_values(array_unique(array_filter($uuids))) as $uuid) {
|
||||
$staff = $this->staffRepo->findByUuid((string) $uuid);
|
||||
if ($staff === null || $staff->getEntityType() !== $entityType || $staff->getEntityId() !== $entityId) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'پرسنل انتخابشده متعلق به شما نیست', 422, 'staff_uuids');
|
||||
}
|
||||
$members[] = $staff;
|
||||
}
|
||||
|
||||
$item->setStaffMembers($members);
|
||||
return null;
|
||||
}
|
||||
|
||||
private function resolveEntity(User $user): array
|
||||
{
|
||||
if ($user->hasRole('ROLE_DOCTOR')) {
|
||||
|
||||
Reference in New Issue
Block a user