feat(clinic): enforce maximum gallery image limit of 5 and update validation logic

This commit is contained in:
hamed
2026-06-19 01:15:27 +03:30
parent dd1ca98cb4
commit e7be54b84e
3 changed files with 45 additions and 7 deletions
+18 -4
View File
@@ -34,6 +34,7 @@ L.Icon.Default.mergeOptions({
const HUES_LIST = [256, 205, 162, 295, 272]; const HUES_LIST = [256, 205, 162, 295, 272];
const IRAN_CENTER: [number, number] = [32.4279, 53.6880]; const IRAN_CENTER: [number, number] = [32.4279, 53.6880];
const MAX_GALLERY_IMAGES = 5;
// ── Types ────────────────────────────────────────────────────────────────── // ── Types ──────────────────────────────────────────────────────────────────
@@ -623,10 +624,21 @@ export default function ClinicDetailPage() {
}; };
const handleGalleryUpload = async (files: FileList) => { const handleGalleryUpload = async (files: FileList) => {
const existing = (clinic?.images_clinic ?? []).filter(img => img?.url);
const remaining = MAX_GALLERY_IMAGES - existing.length;
if (remaining <= 0) {
toast.error(`گالری حداکثر ${MAX_GALLERY_IMAGES} عکس می‌تواند داشته باشد`);
return;
}
const selected = Array.from(files);
if (selected.length > remaining) {
toast.error(`فقط ${remaining} عکس دیگر می‌توانید اضافه کنید (حداکثر ${MAX_GALLERY_IMAGES})`);
}
const toUpload = selected.slice(0, remaining);
setGalleryUploading(true); setGalleryUploading(true);
try { try {
const uploaded: { url: string }[] = []; const uploaded: { url: string }[] = [];
for (const file of Array.from(files)) { for (const file of toUpload) {
const res = await fetch('/file/upload/clinic_pro/clinic/field_image_clinic', { const res = await fetch('/file/upload/clinic_pro/clinic/field_image_clinic', {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -640,7 +652,6 @@ export default function ClinicDetailPage() {
if (json?.data?.url) uploaded.push({ url: json.data.url }); if (json?.data?.url) uploaded.push({ url: json.data.url });
} }
if (uploaded.length > 0 && clinic) { if (uploaded.length > 0 && clinic) {
const existing = (clinic.images_clinic ?? []).filter(img => img?.url);
await api.patch(`/api/v1/clinic/${uuid}`, { image_clinic: [...existing, ...uploaded] }); await api.patch(`/api/v1/clinic/${uuid}`, { image_clinic: [...existing, ...uploaded] });
toast.success(`${uploaded.length} تصویر اضافه شد`); toast.success(`${uploaded.length} تصویر اضافه شد`);
qc.invalidateQueries({ queryKey: ['clinic-detail', uuid] }); qc.invalidateQueries({ queryKey: ['clinic-detail', uuid] });
@@ -930,8 +941,11 @@ export default function ClinicDetailPage() {
{/* Gallery */} {/* Gallery */}
<div className="card card-pad"> <div className="card card-pad">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 14 }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 14 }}>
<b style={{ fontSize: 14 }}>گالری تصاویر</b> <b style={{ fontSize: 14 }}>
<button className="btn ghost sm" onClick={() => galleryInputRef.current?.click()} disabled={galleryUploading}> گالری تصاویر ({formatNumber((clinic.images_clinic ?? []).filter(i => i?.url).length)}/{formatNumber(MAX_GALLERY_IMAGES)})
</b>
<button className="btn ghost sm" onClick={() => galleryInputRef.current?.click()}
disabled={galleryUploading || (clinic.images_clinic ?? []).filter(i => i?.url).length >= MAX_GALLERY_IMAGES}>
<PlusIcon style={{ width: 14, height: 14 }} /> <PlusIcon style={{ width: 14, height: 14 }} />
{galleryUploading ? 'در حال آپلود...' : 'افزودن تصویر'} {galleryUploading ? 'در حال آپلود...' : 'افزودن تصویر'}
</button> </button>
+1 -1
View File
@@ -46,7 +46,7 @@ Create a new clinic.
| `longitude` | float | ❌ | Longitude for map | | `longitude` | float | ❌ | Longitude for map |
| `state` | string | ❌ | Province name | | `state` | string | ❌ | Province name |
| `city` | string | ❌ | City name | | `city` | string | ❌ | City name |
| `image_clinic` | object[] | ❌ | Gallery images `[{url: "..."}]` | | `image_clinic` | object[] | ❌ | Gallery images `[{url: "..."}]`**max 5**; more returns `ERR_VALIDATION_001` (422) |
| `clinic_logo` | string | ❌ | Logo URL | | `clinic_logo` | string | ❌ | Logo URL |
| `doctors` | string[] | ❌ | Doctor UUIDs to associate | | `doctors` | string[] | ❌ | Doctor UUIDs to associate |
| `specialties` | integer[] | ❌ | Specialty IDs | | `specialties` | integer[] | ❌ | Specialty IDs |
+26 -2
View File
@@ -30,6 +30,8 @@ use Symfony\Component\Uid\Uuid;
#[OA\Tag(name: 'Clinics')] #[OA\Tag(name: 'Clinics')]
class ClinicController extends BaseController class ClinicController extends BaseController
{ {
private const MAX_GALLERY_IMAGES = 5;
public function __construct( public function __construct(
private readonly ClinicRepository $clinicRepo, private readonly ClinicRepository $clinicRepo,
private readonly DoctorRepository $doctorRepo, private readonly DoctorRepository $doctorRepo,
@@ -91,6 +93,9 @@ class ClinicController extends BaseController
public function create(Request $request, #[CurrentUser] User $user): JsonResponse public function create(Request $request, #[CurrentUser] User $user): JsonResponse
{ {
$data = json_decode($request->getContent(), true) ?? []; $data = json_decode($request->getContent(), true) ?? [];
if (($err = $this->validateGallerySize($data)) !== null) {
return $err;
}
$clinic = new Clinic($user); $clinic = new Clinic($user);
$this->hydrateClinic($clinic, $data); $this->hydrateClinic($clinic, $data);
$this->clinicRepo->save($clinic); $this->clinicRepo->save($clinic);
@@ -209,6 +214,9 @@ class ClinicController extends BaseController
} }
$data = json_decode($request->getContent(), true) ?? []; $data = json_decode($request->getContent(), true) ?? [];
if (($err = $this->validateGallerySize($data)) !== null) {
return $err;
}
$this->hydrateClinic($clinic, $data); $this->hydrateClinic($clinic, $data);
$this->clinicRepo->save($clinic); $this->clinicRepo->save($clinic);
@@ -457,6 +465,22 @@ class ClinicController extends BaseController
// ── Helpers ─────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────
private function validateGallerySize(array $data): ?JsonResponse
{
if (array_key_exists('image_clinic', $data)
&& is_array($data['image_clinic'])
&& count($data['image_clinic']) > self::MAX_GALLERY_IMAGES
) {
return $this->error(
ErrorCodes::ERR_VALIDATION_001,
'گالری تصاویر حداکثر ' . self::MAX_GALLERY_IMAGES . ' عکس می‌تواند داشته باشد',
422,
'image_clinic',
);
}
return null;
}
private function hydrateClinic(Clinic $clinic, array $data): void private function hydrateClinic(Clinic $clinic, array $data): void
{ {
if (array_key_exists('name', $data)) $clinic->setName($data['name']); if (array_key_exists('name', $data)) $clinic->setName($data['name']);
@@ -476,9 +500,9 @@ class ClinicController extends BaseController
$clinic->setCityId((int) $data['city'][0]); $clinic->setCityId((int) $data['city'][0]);
} }
// Images stored as JSON (from upload response) // Images stored as JSON (from upload response); gallery is capped at 5.
if (array_key_exists('image_clinic', $data) && is_array($data['image_clinic'])) { if (array_key_exists('image_clinic', $data) && is_array($data['image_clinic'])) {
$clinic->setImagesClinic($data['image_clinic']); $clinic->setImagesClinic(array_slice(array_values($data['image_clinic']), 0, self::MAX_GALLERY_IMAGES));
} }
if (array_key_exists('clinic_logo', $data)) { if (array_key_exists('clinic_logo', $data)) {
$logo = $data['clinic_logo']; $logo = $data['clinic_logo'];