feat: add support for individual consumable items in service items

- Introduced `consumables` field in `ServiceItem` to allow multiple individual items alongside inventory packages.
- Created `ServiceItemConsumable` entity to manage individual consumable items linked to a service.
- Updated `ServiceItemController` to handle CRUD operations for consumables.
- Enhanced `ServiceDetailPage` and `ServiceItemFormModal` to display and manage consumables.
- Added tests to ensure functionality for adding, updating, and validating consumables.
- Updated API documentation to reflect changes in service item structure and consumables.
This commit is contained in:
hamed
2026-07-18 12:34:42 +03:30
parent c4a661b542
commit c13cc57c48
11 changed files with 530 additions and 40 deletions
@@ -17,6 +17,7 @@ use App\ClinicService\Service\ServiceItemAuditService;
use App\ClinicService\Service\TariffService;
use App\Clinic\Repository\ClinicRepository;
use App\Doctor\Repository\DoctorRepository;
use App\Inventory\Repository\InventoryItemRepository;
use App\Inventory\Repository\InventoryPackageRepository;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Controller\BaseController;
@@ -44,6 +45,7 @@ class ClinicServiceController extends BaseController
private readonly TariffRepository $tariffRepo,
private readonly TariffService $tariffService,
private readonly InventoryPackageRepository $packageRepo,
private readonly InventoryItemRepository $inventoryItemRepo,
private readonly ServiceItemAuditService $auditService,
private readonly ServiceItemAuditLogRepository $auditLogRepo,
private readonly EntityManagerInterface $em,
@@ -98,6 +100,39 @@ class ClinicServiceController extends BaseController
return null;
}
/**
* `consumables: [{item_uuid, amount}]` را روی خدمت می‌نشاند. آرایه‌ی خالی یعنی حذف
* همه‌ی اقلام. هر قلم باید متعلق به همان مطب/کلینیک باشد.
*/
private function applyConsumables(ServiceItem $item, array $data, string $entityType, ?int $entityId): ?JsonResponse
{
if (!array_key_exists('consumables', $data)) {
return null;
}
$lines = [];
foreach ((array) ($data['consumables'] ?? []) as $raw) {
$uuid = is_array($raw) ? ($raw['item_uuid'] ?? null) : null;
if ($uuid === null || $uuid === '') {
continue;
}
$inventoryItem = $this->inventoryItemRepo->findByUuid((string) $uuid);
if ($inventoryItem === null
|| $inventoryItem->getEntityType() !== $entityType
|| $inventoryItem->getEntityId() !== $entityId
) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'کالای انتخاب‌شده یافت نشد', 422, 'consumables');
}
$lines[] = ['item' => $inventoryItem, 'amount' => max(1, (int) ($raw['amount'] ?? 1))];
}
$item->replaceConsumables($lines);
return null;
}
// ── Service Sections ─────────────────────────────────────────────────────
#[Route('/api/v1/service-sections', methods: ['GET'])]
@@ -275,6 +310,10 @@ class ClinicServiceController extends BaseController
if ($packageError !== null) {
return $packageError;
}
$consumableError = $this->applyConsumables($item, $data, $entityType, $entityId);
if ($consumableError !== null) {
return $consumableError;
}
$this->itemRepo->save($item);
@@ -325,6 +364,10 @@ class ClinicServiceController extends BaseController
if ($packageError !== null) {
return $packageError;
}
$consumableError = $this->applyConsumables($item, $data, $entityType, $entityId);
if ($consumableError !== null) {
return $consumableError;
}
$this->itemRepo->save($item);