feat: create patient record without prior signup
Extend POST /api/v1/patient to resolve user by uuid or mobile, and create a new ROLE_USER (name + optional national code, no password) when no user exists. Admin modal shows a new-patient form on lookup miss. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# تعریف بیمار جدید بدون ثبتنام قبلی در سایت
|
||||
|
||||
## پروژه
|
||||
|
||||
`clinicpro` (backend + admin frontend)
|
||||
|
||||
## زمینه
|
||||
|
||||
در حال حاضر برای ایجاد پروندهی بیمار، کاربر باید **از قبل در سیستم ثبتنام کرده باشد**: مودال «ایجاد پرونده بیمار» با شماره موبایل جستجو میکند و اگر `User` پیدا نشد، خطا میدهد.
|
||||
|
||||
```php
|
||||
// src/Patient/Controller/PatientController.php — searchUser
|
||||
$patient = $this->userRepo->findByMobile($mobile);
|
||||
if ($patient === null) {
|
||||
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'کاربری با این شماره یافت نشد', 404);
|
||||
}
|
||||
```
|
||||
|
||||
و `create` فقط با `user_uuid` کار میکند:
|
||||
|
||||
```php
|
||||
$userUuid = trim($data['user_uuid'] ?? '');
|
||||
$patient = $this->userRepo->findByUuid($userUuid);
|
||||
```
|
||||
|
||||
## مشکل / هدف
|
||||
|
||||
کلینیک/مطب باید بتواند بیمار جدیدی تعریف کند که **هرگز در سایت ثبتنام نکرده**. یعنی اگر با شماره موبایل کاربری پیدا نشد، بهجای خطا، امکان ساخت `User` جدید (بههمراه پروفایل پایه: نام، کد ملی، جنسیت و …) و سپس ساخت `PatientRecord` فراهم شود.
|
||||
|
||||
## فایلهای مرتبط
|
||||
|
||||
| فایل | نقش |
|
||||
|------|-----|
|
||||
| `src/Patient/Controller/PatientController.php` | `searchUser` + `create` |
|
||||
| `src/Patient/Service/PatientService.php` | منطق ساخت پرونده |
|
||||
| `src/Auth/Entity/User.php` | کاربر؛ دارای `nationalCode`, `realName`, `mobileNumber` |
|
||||
| `src/UserProfile/Entity/UserProfile.php` | پروفایل کامل بیمار (family, gender, dateOfBirth, insurance ids و …) |
|
||||
| `src/Auth/Repository/UserRepository.php` | `findByMobile`, `findByUuid`, `save` |
|
||||
| `assets/admin/pages/MyPatientsPage.tsx` | مودال «ایجاد پرونده بیمار» (state: `createRecordOpen`, `searchMobile`, `foundUser`) |
|
||||
| `docs/api/patient.md` | مستندات API بیمار |
|
||||
|
||||
## وضعیت فعلی
|
||||
|
||||
- مودال فعلی: شماره موبایل میگیرد → `GET /api/v1/patient/search-user?mobile=` → اگر یافت شد کارت نتیجه + دکمهی «ایجاد پرونده» (`POST /api/v1/patient` با `user_uuid`).
|
||||
- اگر یافت نشد: فقط پیغام خطا «کاربری با این شماره یافت نشد»؛ راهی برای ساخت بیمار جدید نیست.
|
||||
- `User` ساختنی است با `new User($mobileNumber)` و `setRealName`, `setNationalCode`.
|
||||
- `UserProfile` فیلدهای دموگرافیک کامل دارد (`family`, `gender`, `dateOfBirth`, `basicInsuranceId`, `supplementaryInsuranceId` …).
|
||||
|
||||
## وظایف
|
||||
|
||||
### ۱. Backend — endpoint ساخت بیمار جدید
|
||||
|
||||
دو رویکرد ممکن (یکی را انتخاب و توضیح بده):
|
||||
|
||||
- **الف)** گسترش `POST /api/v1/patient`: اگر `user_uuid` نبود ولی `mobile` + `name` (+ فیلدهای اختیاری پروفایل) آمد، `User` جدید بساز (با نقش `ROLE_USER`)، در صورت نیاز `UserProfile` بساز، سپس `PatientRecord`.
|
||||
- **ب)** endpoint جدا `POST /api/v1/patient/new-user`.
|
||||
|
||||
> توصیه: رویکرد الف. در `create`: اول با `user_uuid`؛ اگر نبود، با `mobile` جستجو کن؛ اگر `User` نبود و `name` آمده بود، بساز.
|
||||
|
||||
نکات:
|
||||
- اگر شماره موبایل قبلاً وجود دارد (کاربر ثبتنامکرده)، همان `User` استفاده شود (نه duplicate).
|
||||
- validation: موبایل `^09\d{9}$`, کد ملی در صورت ارسال `^\d{10}$`.
|
||||
- فیلدهای پروفایل اختیاری ذخیره شوند (در `User` یا `UserProfile` طبق مدل واقعی).
|
||||
|
||||
### ۲. Admin Frontend — مودال
|
||||
|
||||
مودال «ایجاد پرونده بیمار» را گسترش بده:
|
||||
|
||||
- بعد از جستجو، اگر کاربر **یافت نشد**، بهجای خطای صرف، یک فرم «ساخت بیمار جدید» نشان بده (نام، کد ملی، جنسیت، و فیلدهای پایه) با موبایل از پیش پرشده.
|
||||
- دکمهی «ایجاد پرونده» این بار `User` جدید را هم میسازد.
|
||||
- اگر یافت شد، رفتار فعلی حفظ شود.
|
||||
|
||||
### ۳. مستندات
|
||||
|
||||
`docs/api/patient.md` را با تغییر `POST /api/v1/patient` (یا endpoint جدید) بهروز کن: body جدید، حالتهای یافتشد/ساخت جدید، error codeها.
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- entity owner از `#[CurrentUser]` resolve میشود (`PatientController::resolveEntity`).
|
||||
- duplicate موبایل را مدیریت کن.
|
||||
- این پرامپت با `patient-record-profile-binding.md` همپوشانی دارد (هر دو روی پروفایل بیمار کار میکنند)؛ اگر آن اجرا شده، از همان ساختار `UserProfile` استفاده کن.
|
||||
- بیمار ساختهشده بدون رمز عبور است (بعداً میتواند با OTP وارد شود)؛ مطمئن شو ساخت `User` بدون `passwordHash` مجاز است (هست — `nullable: true`).
|
||||
@@ -142,6 +142,8 @@ function MyPatientsPageInner() {
|
||||
} | null>(null);
|
||||
const [recordNationalCode, setRecordNationalCode] = useState("");
|
||||
const [searchError, setSearchError] = useState("");
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
const [newPatientName, setNewPatientName] = useState("");
|
||||
const mobileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const servicesTotal = selectedServices.reduce(
|
||||
@@ -285,22 +287,28 @@ function MyPatientsPageInner() {
|
||||
),
|
||||
onSuccess: (res: any) => {
|
||||
setFoundUser(res?.data);
|
||||
setRecordNationalCode(res?.data?.national_code ?? "");
|
||||
setNotFound(false);
|
||||
setSearchError("");
|
||||
},
|
||||
onError: () => {
|
||||
setFoundUser(null);
|
||||
setSearchError("کاربری با این شماره در سیستم یافت نشد");
|
||||
setNotFound(true);
|
||||
setSearchError("");
|
||||
},
|
||||
});
|
||||
|
||||
const createRecordMut = useMutation({
|
||||
mutationFn: (userUuid: string) =>
|
||||
api.post("/api/v1/patient", { user_uuid: userUuid }),
|
||||
mutationFn: (body: Record<string, unknown>) =>
|
||||
api.post("/api/v1/patient", body),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["patients"] });
|
||||
setCreateRecordOpen(false);
|
||||
setSearchMobile("");
|
||||
setFoundUser(null);
|
||||
setRecordNationalCode("");
|
||||
setNotFound(false);
|
||||
setNewPatientName("");
|
||||
setSearchError("");
|
||||
toast.success("پرونده بیمار ایجاد شد");
|
||||
},
|
||||
@@ -315,13 +323,32 @@ function MyPatientsPageInner() {
|
||||
}
|
||||
setSearchError("");
|
||||
setFoundUser(null);
|
||||
setNotFound(false);
|
||||
searchUserMut.mutate(digits);
|
||||
};
|
||||
|
||||
const handleCreateRecord = () => {
|
||||
if (foundUser) {
|
||||
createRecordMut.mutate({
|
||||
user_uuid: foundUser.uuid,
|
||||
national_code: recordNationalCode || undefined,
|
||||
});
|
||||
} else if (notFound) {
|
||||
createRecordMut.mutate({
|
||||
mobile: searchMobile.replace(/\D/g, ""),
|
||||
name: newPatientName.trim(),
|
||||
national_code: recordNationalCode || undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateRecordClose = () => {
|
||||
setCreateRecordOpen(false);
|
||||
setSearchMobile("");
|
||||
setFoundUser(null);
|
||||
setRecordNationalCode("");
|
||||
setNotFound(false);
|
||||
setNewPatientName("");
|
||||
setSearchError("");
|
||||
};
|
||||
|
||||
@@ -493,12 +520,11 @@ function MyPatientsPageInner() {
|
||||
<button
|
||||
className="btn primary sm"
|
||||
disabled={
|
||||
!foundUser || createRecordMut.isPending
|
||||
}
|
||||
onClick={() =>
|
||||
foundUser &&
|
||||
createRecordMut.mutate(foundUser.uuid)
|
||||
createRecordMut.isPending ||
|
||||
(!foundUser &&
|
||||
!(notFound && newPatientName.trim()))
|
||||
}
|
||||
onClick={handleCreateRecord}
|
||||
>
|
||||
{createRecordMut.isPending
|
||||
? "در حال ایجاد..."
|
||||
@@ -685,6 +711,80 @@ function MyPatientsPageInner() {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{notFound && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 16,
|
||||
padding: 14,
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: "var(--r)",
|
||||
background: "var(--surface)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12.5,
|
||||
color: "var(--text-3)",
|
||||
lineHeight: 1.7,
|
||||
}}
|
||||
>
|
||||
کاربری با این شماره یافت نشد. برای ثبت بیمار جدید،
|
||||
نام را وارد کنید.
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 5,
|
||||
}}
|
||||
>
|
||||
<label
|
||||
style={{ fontSize: 12.5, fontWeight: 600 }}
|
||||
>
|
||||
نام و نام خانوادگی *
|
||||
</label>
|
||||
<input
|
||||
className="input"
|
||||
value={newPatientName}
|
||||
onChange={(e) =>
|
||||
setNewPatientName(e.target.value)
|
||||
}
|
||||
placeholder="مثال: محمد محمدی"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 5,
|
||||
}}
|
||||
>
|
||||
<label
|
||||
style={{ fontSize: 12.5, fontWeight: 600 }}
|
||||
>
|
||||
کد ملی (اختیاری)
|
||||
</label>
|
||||
<input
|
||||
className="input"
|
||||
dir="ltr"
|
||||
inputMode="numeric"
|
||||
maxLength={10}
|
||||
value={recordNationalCode}
|
||||
onChange={(e) =>
|
||||
setRecordNationalCode(
|
||||
e.target.value.replace(/\D/g, ""),
|
||||
)
|
||||
}
|
||||
placeholder="۱۰ رقم"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
+14
-3
@@ -67,14 +67,26 @@ POST /api/v1/patient
|
||||
|
||||
Creates a patient record for a user under the current entity. If the record already exists, returns the existing record (idempotent).
|
||||
|
||||
سه حالت پشتیبانی میشود:
|
||||
1. **کاربر ثبتنامکرده با uuid:** `user_uuid` ارسال شود.
|
||||
2. **کاربر ثبتنامکرده با موبایل:** `mobile` ارسال شود (کاربر موجود پیدا میشود).
|
||||
3. **بیمار جدید بدون ثبتنام:** `mobile` + `name` ارسال شود؛ اگر کاربری با آن موبایل نباشد، `User` جدید (نقش `ROLE_USER`، بدون رمز عبور) ساخته میشود سپس پرونده.
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"user_uuid": "string (required)"
|
||||
"user_uuid": "string (اختیاری)",
|
||||
"mobile": "09xxxxxxxxx (اختیاری — برای جستجو یا ساخت بیمار جدید)",
|
||||
"name": "string (الزامی فقط هنگام ساخت بیمار جدید)",
|
||||
"national_code": "string (اختیاری، ۱۰ رقم)"
|
||||
}
|
||||
```
|
||||
|
||||
- اگر `user_uuid` و `mobile` هر دو خالی باشند → خطا.
|
||||
- `national_code` فقط وقتی روی کاربر ست میشود که کاربر کد ملی نداشته باشد.
|
||||
- موبایل تکراری duplicate نمیسازد؛ همان کاربر استفاده میشود.
|
||||
|
||||
**Response 201:**
|
||||
|
||||
```json
|
||||
@@ -96,8 +108,7 @@ Creates a patient record for a user under the current entity. If the record alre
|
||||
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_VALIDATION_001` | 422 | `user_uuid` missing |
|
||||
| `ERR_NOT_FOUND_001` | 404 | User not found |
|
||||
| `ERR_VALIDATION_001` | 422 | `user_uuid`/`mobile` خالی، یا موبایل/کد ملی نامعتبر، یا نام برای بیمار جدید خالی |
|
||||
| `ERR_SUBSCRIPTION_REQUIRED` | 403 | No `patient_records` feature |
|
||||
|
||||
---
|
||||
|
||||
@@ -92,21 +92,36 @@ class PatientController extends BaseController
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
$userUuid = trim($data['user_uuid'] ?? '');
|
||||
|
||||
if ($userUuid === '') {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'user_uuid الزامی است', 422);
|
||||
}
|
||||
|
||||
$patient = $this->userRepo->findByUuid($userUuid);
|
||||
if ($patient === null) {
|
||||
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'کاربر یافت نشد', 404);
|
||||
}
|
||||
$mobile = trim($data['mobile'] ?? '');
|
||||
$name = trim($data['name'] ?? '');
|
||||
|
||||
$nationalCode = trim((string) ($data['national_code'] ?? ''));
|
||||
if ($nationalCode !== '' && $patient->getNationalCode() === null) {
|
||||
if (!preg_match('/^\d{10}$/', $nationalCode)) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'کد ملی باید ۱۰ رقم باشد', 422);
|
||||
if ($nationalCode !== '' && !preg_match('/^\d{10}$/', $nationalCode)) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'کد ملی باید ۱۰ رقم باشد', 422);
|
||||
}
|
||||
|
||||
$patient = null;
|
||||
if ($userUuid !== '') {
|
||||
$patient = $this->userRepo->findByUuid($userUuid);
|
||||
} elseif ($mobile !== '') {
|
||||
if (!preg_match('/^09\d{9}$/', $mobile)) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'شماره موبایل نامعتبر است', 422);
|
||||
}
|
||||
$patient = $this->userRepo->findByMobile($mobile);
|
||||
}
|
||||
|
||||
// بیمار جدید بدون ثبتنام قبلی: موبایل + نام آمده ولی کاربری وجود ندارد
|
||||
if ($patient === null) {
|
||||
if ($mobile === '' || $name === '') {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'برای ساخت بیمار جدید، شماره موبایل و نام الزامی است', 422);
|
||||
}
|
||||
$patient = new User($mobile);
|
||||
$patient->setRealName($name);
|
||||
if ($nationalCode !== '') {
|
||||
$patient->setNationalCode($nationalCode);
|
||||
}
|
||||
$this->userRepo->save($patient);
|
||||
} elseif ($nationalCode !== '' && $patient->getNationalCode() === null) {
|
||||
$patient->setNationalCode($nationalCode);
|
||||
$this->userRepo->save($patient);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user