Files
clinicpro/src/Resource/Controller/ResourceController.php
T
hamedandClaude Opus 5 000cf70761 feat(resources): warn before switching a resource off, and pick dates in Jalali
The deactivation warning was blocked on task 07: there was no way to count "the
appointments on this resource" until occupancy rows linked the two. They do
now, so GET /resource/{uuid} returns upcoming_appointments. It stays off the
list endpoint, where it would be one count query per row.

It is a warning, not a block, and the wording says so: switching a resource off
does not cancel anything, it only removes the resource from future searches.
The panel shows it the moment the "active" box is unticked.

The calendar's exception range still used <input type="date">, which is
Gregorian. Operators say dates in Jalali, and the mental conversion is exactly
where an exception gets recorded a day off. PersianDateInput takes the same
YYYY-MM-DD string, so this is a drop-in swap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 13:45:25 +03:30

147 lines
6.1 KiB
PHP

<?php
namespace App\Resource\Controller;
use App\Appointment\Availability\Repository\ResourceOccupancyRepository;
use App\Auth\Entity\User;
use App\Resource\Entity\ClinicResource;
use App\Resource\Repository\ClinicResourceRepository;
use App\Resource\Service\ResourceContext;
use App\Resource\Service\ResourceService;
use App\Resource\Service\SkillAssignmentService;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Controller\BaseController;
use OpenApi\Attributes as OA;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[OA\Tag(name: 'Resource')]
#[IsGranted('IS_AUTHENTICATED_FULLY')]
class ResourceController extends BaseController
{
use ResourcePermissionTrait;
public function __construct(
private readonly ResourceContext $context,
private readonly ClinicResourceRepository $resources,
private readonly ResourceService $service,
private readonly SkillAssignmentService $skills,
private readonly ResourceOccupancyRepository $occupancy,
) {}
#[Route('/api/v1/resources', name: 'resource_list', methods: ['GET'])]
public function list(#[CurrentUser] User $user, Request $request): JsonResponse
{
$this->denyUnlessGranted($user, 'view');
[$entityType, $entityId] = $this->context->pair($user);
$addressUuid = $request->query->get('address_uuid');
$typeUuid = $request->query->get('type_uuid');
$activeParam = $request->query->get('active');
$filters = [
'address' => is_string($addressUuid) && $addressUuid !== '' ? $this->context->address($user, $addressUuid) : null,
'type' => is_string($typeUuid) && $typeUuid !== '' ? $this->context->type($user, $typeUuid) : null,
'active' => $activeParam === null || $activeParam === '' ? null : filter_var($activeParam, FILTER_VALIDATE_BOOL),
'skillUuid' => $request->query->get('skill_uuid') ?: null,
];
// مهارتِ محیط دیگر نباید بی‌صدا «هیچ نتیجه» بدهد؛ ۴۰۴ صریح است.
if ($filters['skillUuid'] !== null) {
$this->context->skill($user, $filters['skillUuid']);
}
return $this->success(array_map(
static fn (ClinicResource $r): array => $r->toArray(),
$this->resources->findForPair($entityType, $entityId, $filters),
));
}
#[Route('/api/v1/resource', name: 'resource_create', methods: ['POST'])]
public function create(#[CurrentUser] User $user, Request $request): JsonResponse
{
$this->denyUnlessGranted($user, 'update');
$data = json_decode($request->getContent(), true);
if (!is_array($data) || !is_string($data['address_uuid'] ?? null)) {
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'فیلد address_uuid الزامی است', 422, 'address_uuid');
}
if (!is_string($data['type_uuid'] ?? null)) {
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'فیلد type_uuid الزامی است', 422, 'type_uuid');
}
$address = $this->context->address($user, $data['address_uuid']);
$type = $this->context->type($user, $data['type_uuid']);
return $this->success($this->service->create($address, $type, $data)->toArray(), 201);
}
#[Route('/api/v1/resource/{uuid}', name: 'resource_show', methods: ['GET'])]
public function show(#[CurrentUser] User $user, string $uuid): JsonResponse
{
$this->denyUnlessGranted($user, 'view');
$resource = $this->context->resource($user, $uuid);
// شمار نوبت‌های آینده کنار خودِ منبع می‌آید تا پنل بتواند پیش از خاموش‌کردنش
// هشدار بدهد. در فهرست نمی‌آید — آنجا یک کوئری per ردیف می‌شد.
return $this->success($resource->toArray() + [
'upcoming_appointments' => $this->occupancy->countUpcomingAppointments((int) $resource->getId()),
]);
}
#[Route('/api/v1/resource/{uuid}', name: 'resource_update', methods: ['PATCH'])]
public function update(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse
{
$this->denyUnlessGranted($user, 'update');
$data = json_decode($request->getContent(), true);
if (!is_array($data)) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'بدنهٔ درخواست نامعتبر است', 422);
}
$resource = $this->context->resource($user, $uuid);
if (is_string($data['type_uuid'] ?? null)) {
$resource->setType($this->context->type($user, $data['type_uuid']));
}
return $this->success($this->service->update($resource, $data)->toArray());
}
#[Route('/api/v1/resource/{uuid}', name: 'resource_delete', methods: ['DELETE'])]
public function delete(#[CurrentUser] User $user, string $uuid): JsonResponse
{
$this->denyUnlessGranted($user, 'update');
$this->service->delete($this->context->resource($user, $uuid));
return $this->success(null);
}
/** جایگزینی کامل مهارت‌های منبع: مهارتی که در بدنه نیست، برداشته می‌شود. */
#[Route('/api/v1/resource/{uuid}/skills', name: 'resource_skills_replace', methods: ['PUT'])]
public function replaceSkills(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse
{
$this->denyUnlessGranted($user, 'update');
$data = json_decode($request->getContent(), true);
if (!is_array($data) || !is_array($data['skills'] ?? null)) {
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'فیلد skills الزامی است', 422, 'skills');
}
$resource = $this->context->resource($user, $uuid);
$this->skills->replace($user, $resource, $data['skills']);
return $this->success($resource->toArray());
}
}