feat: implement pre-registration system for doctors and clinics with public endpoint and admin management
This commit is contained in:
@@ -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 بهروزرسانی کن
|
||||
Reference in New Issue
Block a user