feat: implement pre-registration system for doctors and clinics with public endpoint and admin management

This commit is contained in:
hamed
2026-06-12 11:45:16 +03:30
parent 484d3024c1
commit 92cb834c22
+212
View File
@@ -0,0 +1,212 @@
# Pre-Registration Feature
## هدف
پیاده‌سازی سیستم پیش ثبت‌نام برای دکتر و کلینیک:
- روی صفحه اصلی (`/`) دو دکمه باشد:
- **ورود به پنل** → `/admin` (برای کسانی که قبلاً ثبت‌نام شده‌اند)
- **ثبت نام** → فرم پیش ثبت‌نام (فقط برای دکتر و کلینیک)
- دکتر/کلینیک فرم را پر می‌کند
- ادمین در پنل درخواست‌ها را می‌بیند و تأیید یا رد می‌کند
- پس از تأیید: کاربر و entity مربوطه ساخته می‌شود و نام‌کاربری+پسورد از طریق SMS ارسال می‌شود
---
## قابلیت‌ها
### قابلیت ۱ — Entity و Migration: PreRegistration
ایجاد Entity جدید `src/Auth/Entity/PreRegistration.php` با فیلدهای:
- `id` (int, PK)
- `uuid` (string, unique)
- `type` (string: `independent_doctor` | `doctor_with_clinic` | `clinic_manager`) — نوع حساب (توضیح در پایین)
- `name` (string, 255) — نام کامل
- `mobile` (string, 20) — شماره موبایل
- `info` (text, nullable) — توضیحات / اطلاعات اضافه (تخصص، سابقه، ...)
- `status` (string: `pending` | `approved` | `rejected`) — default: `pending`
- `adminNote` (text, nullable) — یادداشت ادمین هنگام رد کردن
- `createdAt` (int) — Unix timestamp
- `updatedAt` (int) — Unix timestamp
بعد از ایجاد Entity: حتماً `doctrine:migrations:diff` و `doctrine:migrations:migrate` اجرا شود.
---
### قابلیت ۲ — Backend: Public Pre-Registration Endpoint
**`POST /api/v1/pre-registration`** — endpoint عمومی (بدون auth)
فایل: `src/Auth/Controller/PreRegistrationController.php` (class جدید، extends BaseController)
Request body:
```json
{
"type": "independent_doctor", // independent_doctor | doctor_with_clinic | clinic_manager — الزامی
"name": "دکتر احمدی", // الزامی
"mobile": "09121234567", // الزامی
"info": "متخصص داخلی، ۱۰ سال سابقه" // اختیاری
}
```
مفهوم `type` و نقش‌های دریافتی پس از تأیید:
| مقدار | متن نمایشی در فرم | نقش‌ها | entity ساخته می‌شود |
|-------|-------------------|--------|---------------------|
| `independent_doctor` | «دکتر هستم» | `ROLE_DOCTOR` | Doctor |
| `doctor_with_clinic` | «دکتر هستم و کلینیک دارم و با چند دکتر همکاری می‌کنم» | `ROLE_DOCTOR` + `ROLE_CLINIC` | Doctor + Clinic |
| `clinic_manager` | «تنها مدیر کلینیک هستم» | `ROLE_CLINIC` | Clinic |
Validation:
- `type` باید یکی از `independent_doctor`، `doctor_with_clinic`، یا `clinic_manager` باشد
- `mobile` باید ۱۰ تا ۱۵ کاراکتر باشد
- اگر همین موبایل با status=pending در جدول وجود داشت → خطا: `DUPLICATE_REQUEST` با پیام "درخواست ثبت‌نام شما در حال بررسی است"
Response موفق:
```json
{ "success": true, "data": { "uuid": "...", "status": "pending" } }
```
امنیت: این endpoint باید در `security.yaml` به عنوان عمومی (public) تعریف شود.
---
### قابلیت ۳ — Backend: Admin Pre-Registration Management Endpoints
در فایل `src/Auth/Controller/PreRegistrationController.php` متدهای زیر اضافه شوند:
**`GET /api/v1/admin/pre-registrations`** — لیست درخواست‌ها
- `#[IsGranted('ROLE_ADMIN')]`
- Query params: `page`, `limit`, `status` (pending|approved|rejected|all — default: pending)
- DQL array hydration، برگرداندن: uuid, type, name, mobile, info, status, admin_note, created_at
- Return: `$this->paginated(...)`
**`POST /api/v1/admin/pre-registrations/{uuid}/approve`** — تأیید درخواست
- `#[IsGranted('ROLE_ADMIN')]`
- اقدامات هنگام تأیید:
1. بررسی اینکه status=pending باشد (در غیر این صورت خطا: `ALREADY_PROCESSED`)
2. یک پسورد تصادفی ۸ کاراکتری بسازید (حروف+عدد)
3. اگر user با این mobile وجود ندارد → User جدید بسازید با این mobile و پسورد
4. اگر user وجود دارد → پسورد را reset کنید
5. بر اساس `type` entity و نقش‌ها را بساز:
- `independent_doctor`:
- Doctor entity بساز (firstName/lastName از split نام؛ اگر یک کلمه بود همه firstName)
- به user بده: `ROLE_DOCTOR`
- `doctor_with_clinic`:
- Doctor entity بساز (مانند بالا)
- Clinic entity بساز با `name`=نام درخواست
- به user بده: `ROLE_DOCTOR` + `ROLE_CLINIC`
- `clinic_manager`:
- Clinic entity بساز با `name`=نام درخواست
- به user بده: `ROLE_CLINIC`
در همه حالت‌ها قبل از ساخت entity بررسی کن که قبلاً وجود نداشته باشد (جلوگیری از duplicate)
7. `status` را روی `approved` و `updatedAt` را update کنید
8. یک SMS بفرستید از طریق سرویس SMS موجود در پروژه (کلاس `App\Sms\`) با متن: "به کلینیک پرو خوش آمدید! شماره‌کاربری: {mobile} | رمز عبور: {password} | لینک ورود: https://clinic-pro.ddev.site/admin"
- Response: `$this->success(['message' => 'تأیید شد و اطلاعات ورود ارسال گردید'])`
**`POST /api/v1/admin/pre-registrations/{uuid}/reject`** — رد درخواست
- `#[IsGranted('ROLE_ADMIN')]`
- Request body: `{ "note": "توضیح دلیل رد" }` — اختیاری
- `status` را `rejected` کن، `adminNote` را ذخیره کن
- Response: `$this->success(['message' => 'درخواست رد شد'])`
---
### قابلیت ۴ — Frontend Homepage: دو دکمه در header و hero
فایل: `templates/public/home.html.twig`
**۴الف — جایگزینی کامل `<div class="nav-right">`:**
کل بلوک زیر را حذف کن:
```html
<div class="nav-right">
<button class="nav-ico search" ...>...</button>
<button class="nav-burger nav-toggle" id="navToggle" ...>...</button>
<a href="#download" class="nav-burger" ...>...</a>
</div>
```
و به جای آن این بلوک را قرار بده:
```html
<div class="nav-right">
<button class="nav-burger nav-toggle" id="navToggle" aria-label="منو">
<svg viewBox="0 0 24 24" fill="none"><path d="M4 7h16M4 12h16M4 17h16" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg>
</button>
<button class="btn btn-ghost" id="openRegModal" style="font-size:13px;padding:8px 20px">ثبت نام</button>
<a href="/admin" class="btn btn-blue" style="font-size:13px;padding:8px 20px;text-decoration:none">ورود به پنل</a>
</div>
```
توجه: دکمه hamburger (navToggle) را نگه دار چون JS موبایل به آن وابسته است. دکمه search و لینک download را حذف کن.
**۴ب — دکمه ثبت نام در hero:**
در `<div class="hero-actions reveal d2">` دکمه "دانلود رایگان" را به "ثبت نام" تغییر بده.
توجه: از `class="btn btn-coral"` با `id="openRegModal2"` استفاده کن (id متفاوت از header) و در JS هر دو را به modal وصل کن:
```html
<button class="btn btn-coral" id="openRegModal2">ثبت نام دکتر / کلینیک</button>
<a href="#contact" class="btn btn-blue">تماس با ما</a>
```
**۴ج — Modal پیش ثبت‌نام:**
یک modal HTML در انتهای `<main>` اضافه کن (قبل از `</main>`) با:
- overlay با id=`regOverlay`
- modal box با فرم شامل:
- **انتخاب نوع حساب** — سه کارت قابل کلیک (radio-style) با آیکون و توضیح، به صورت عمودی (stacked):
- `independent_doctor` → عنوان: «دکتر هستم» — زیرعنوان: «مطب شخصی دارم، به تنهایی کار می‌کنم»
- `doctor_with_clinic` → عنوان: «دکتر هستم و کلینیک دارم» — زیرعنوان: «با چند دکتر در یک کلینیک همکاری می‌کنم»
- `clinic_manager` → عنوان: «مدیر کلینیک هستم» — زیرعنوان: «مدیریت کلینیک را دارم، خودم دکتر نیستم»
کارت انتخاب‌شده با border رنگی و پس‌زمینه روشن هایلایت شود
- input: نام کامل
- input: شماره موبایل (dir=ltr)
- textarea: توضیحات اختیاری (تخصص، آدرس، ...)
- دکمه ارسال (disable تا زمانی که type انتخاب نشده)
- CSS inline برای modal (همانند استایل‌های موجود در صفحه: رنگ‌ها `--blue`, `--coral`, border-radius مشابه)
- JS inline:
- باز/بسته شدن modal با کلیک روی هر دو دکمه (`openRegModal` در header و `openRegModal2` در hero) و کلیک روی overlay
- ارسال فرم با `fetch('/api/v1/pre-registration', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({...}) })`
- نمایش پیام موفقیت یا خطا
- بعد از موفقیت: بستن modal و نمایش پیام "درخواست شما ثبت شد. پس از بررسی، اطلاعات ورود از طریق SMS ارسال می‌شود."
---
### قابلیت ۵ — Frontend Admin Panel: صفحه مدیریت درخواست‌ها
**۵الف — ایجاد `assets/admin/pages/PreRegistrationsPage.tsx`:**
- `useQuery` با `queryKey: ['pre-registrations', page, statusFilter]`
- API: `GET /api/v1/admin/pre-registrations?page=...&status=...`
- جدول با ستون‌ها: نام، موبایل، نوع (badge با رنگ‌بندی: «دکتر» آبی برای `independent_doctor` / «دکتر + کلینیک» بنفش برای `doctor_with_clinic` / «مدیر کلینیک» نارنجی برای `clinic_manager`)، وضعیت badge، تاریخ درخواست، ستون اقدامات
- Filter tabs: همه | در انتظار | تأیید شده | رد شده
- برای هر ردیف با status=pending: دکمه‌های تأیید (سبز) و رد (قرمز)
- کلیک تأیید → mutation به `POST /api/v1/admin/pre-registrations/{uuid}/approve` → toast موفقیت
- کلیک رد → یک prompt کوچک (یا یک inline textarea در row) برای وارد کردن دلیل → mutation به `POST /api/v1/admin/pre-registrations/{uuid}/reject` → toast
- بعد از هر mutation: `invalidateQueries(['pre-registrations'])`
**۵ب — اضافه کردن به Sidebar.tsx:**
در بخش admin داخل `buildSections`، در section «مدیریت» یک آیتم اضافه کن:
```
{ to: '/admin/pre-registrations', icon: ClipboardDocumentCheckIcon, label: 'درخواست‌های ثبت‌نام' }
```
(import `ClipboardDocumentCheckIcon` از `@heroicons/react/24/outline`)
**۵ج — اضافه کردن route به App.tsx:**
```tsx
import PreRegistrationsPage from './pages/PreRegistrationsPage';
// ...
<Route path="pre-registrations" element={<RoleRoute roles={['admin']}><PreRegistrationsPage /></RoleRoute>} />
```
---
## نکات اجرایی
### امنیت
- endpoint عمومی `POST /api/v1/pre-registration` را در `config/packages/security.yaml` داخل `public_endpoints` firewall اضافه کن
- endpoint های admin با `#[IsGranted('ROLE_ADMIN')]` محافظت شوند
### SMS
- کلاس SMS موجود را در پروژه پیدا کن (`src/Sms/`) و از آن استفاده کن
- اگر ارسال SMS با خطا مواجه شد، عملیات approve کامل شود (rollback نشود) — فقط یک log warning بزن
### پسورد تصادفی
از `bin2hex(random_bytes(4))` برای تولید پسورد ۸ کاراکتری hex استفاده کن.
### مستندسازی
بعد از اتمام هر قابلیت:
- `docs/api/auth.md` را برای endpoint های جدید pre-registration به‌روزرسانی کن
- `docs/api/admin.md` را برای endpoint های admin به‌روزرسانی کن