feat(patient): add search user functionality by mobile number

This commit is contained in:
hamed
2026-06-15 12:52:45 +03:30
parent 95f5918118
commit d13d8436ab
3 changed files with 153 additions and 4 deletions
+5 -1
View File
@@ -2305,6 +2305,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
onSuccess: () => {
toast.success('آدرس حذف شد'); setDeletingAddrId(null);
qc.invalidateQueries({ queryKey: ['doctor-detail', uuid] });
qc.invalidateQueries({ queryKey: ['available-locations', uuid] });
},
onError: (e: Error) => toast.error(e.message),
});
@@ -2764,7 +2765,10 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
onClose={() => { setAddrModalOpen(false); setEditingAddr(null); }}
existing={editingAddr}
doctorUuid={uuid}
onSaved={() => qc.invalidateQueries({ queryKey: ['doctor-detail', uuid] })}
onSaved={() => {
qc.invalidateQueries({ queryKey: ['doctor-detail', uuid] });
qc.invalidateQueries({ queryKey: ['available-locations', uuid] });
}}
/>
)}
+125 -3
View File
@@ -1,8 +1,8 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useRef } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
MagnifyingGlassIcon, PlusIcon, ChevronRightIcon, PencilIcon, PhoneIcon,
FolderOpenIcon, UsersIcon,
FolderOpenIcon, UsersIcon, UserPlusIcon, CheckCircleIcon,
} from '@heroicons/react/24/outline';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
@@ -66,6 +66,12 @@ function MyPatientsPageInner() {
const [sectionUuid, setSectionUuid] = useState('');
const [itemUuid, setItemUuid] = useState('');
const [createRecordOpen, setCreateRecordOpen] = useState(false);
const [searchMobile, setSearchMobile] = useState('');
const [foundUser, setFoundUser] = useState<{ uuid: string; name: string | null; mobile: string } | null>(null);
const [searchError, setSearchError] = useState('');
const mobileInputRef = useRef<HTMLInputElement>(null);
const servicesTotal = selectedServices.reduce((sum, s) => sum + s.price_rials, 0);
const finalPrice = calcFinalPrice(Number(watchVisit), Number(watchBase), Number(watchSupp), servicesTotal);
@@ -126,6 +132,40 @@ function MyPatientsPageInner() {
onError: (e: any) => toast.error(e.message),
});
const searchUserMut = useMutation({
mutationFn: (mobile: string) => api.get(`/api/v1/patient/search-user?mobile=${encodeURIComponent(mobile)}`),
onSuccess: (res: any) => { setFoundUser(res?.data); setSearchError(''); },
onError: () => { setFoundUser(null); setSearchError('کاربری با این شماره در سیستم یافت نشد'); },
});
const createRecordMut = useMutation({
mutationFn: (userUuid: string) => api.post('/api/v1/patient', { user_uuid: userUuid }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['patients'] });
setCreateRecordOpen(false);
setSearchMobile('');
setFoundUser(null);
setSearchError('');
toast.success('پرونده بیمار ایجاد شد');
},
onError: (e: any) => toast.error(e.message),
});
const handleSearchMobile = () => {
const digits = searchMobile.replace(/\D/g, '');
if (!/^09\d{9}$/.test(digits)) { setSearchError('شماره موبایل معتبر نیست'); return; }
setSearchError('');
setFoundUser(null);
searchUserMut.mutate(digits);
};
const handleCreateRecordClose = () => {
setCreateRecordOpen(false);
setSearchMobile('');
setFoundUser(null);
setSearchError('');
};
const handleAddService = () => {
if (!itemUuid) return;
const found = itemsData?.data?.find((i) => i.uuid === itemUuid);
@@ -192,7 +232,16 @@ function MyPatientsPageInner() {
if (!selectedRecord) {
return (
<>
<PageHeader title="پرونده بیماران" description="مراجعه‌کنندگان ثبت‌شده شما" />
<PageHeader
title="پرونده بیماران"
description="مراجعه‌کنندگان ثبت‌شده شما"
action={
<button className="btn primary sm" onClick={() => setCreateRecordOpen(true)}>
<UserPlusIcon style={{ width: 16 }} />
پرونده جدید
</button>
}
/>
<div style={{ marginBottom: 16 }}>
<div style={{
@@ -234,6 +283,79 @@ function MyPatientsPageInner() {
</>
)}
</div>
{/* Modal ایجاد پرونده دستی */}
<Modal
open={createRecordOpen}
onClose={handleCreateRecordClose}
title="ایجاد پرونده بیمار"
size="sm"
footer={
<>
<button className="btn ghost sm" onClick={handleCreateRecordClose}>انصراف</button>
<button
className="btn primary sm"
disabled={!foundUser || createRecordMut.isPending}
onClick={() => foundUser && createRecordMut.mutate(foundUser.uuid)}
>
{createRecordMut.isPending ? 'در حال ایجاد...' : 'ایجاد پرونده'}
</button>
</>
}
>
<div className="field">
<label>شماره موبایل بیمار</label>
<div style={{ display: 'flex', gap: 8 }}>
<input
ref={mobileInputRef}
value={searchMobile}
placeholder="09123456789"
dir="ltr"
inputMode="numeric"
maxLength={11}
autoFocus
style={{ flex: 1, ...(searchError ? { borderColor: 'var(--danger)' } : {}) }}
onChange={(e) => {
const digits = e.target.value.replace(/\D/g, '');
setSearchMobile(digits);
setFoundUser(null);
setSearchError('');
}}
onKeyDown={(e) => { if (e.key === 'Enter') handleSearchMobile(); }}
/>
<button
className="btn sm"
onClick={handleSearchMobile}
disabled={searchUserMut.isPending}
style={{ flexShrink: 0 }}
>
{searchUserMut.isPending ? '...' : 'جستجو'}
</button>
</div>
{searchError && <span className="field-error">{searchError}</span>}
</div>
{foundUser && (
<div style={{
marginTop: 16,
display: 'flex', alignItems: 'center', gap: 12,
padding: '12px 14px',
background: 'var(--success-subtle, oklch(0.97 0.03 150))',
border: '1px solid oklch(0.88 0.07 150)',
borderRadius: 'var(--r)',
}}>
<CheckCircleIcon style={{ width: 20, color: 'var(--success, #16a34a)', flexShrink: 0 }} />
<div>
<div style={{ fontWeight: 600, fontSize: 14 }}>{foundUser.name ?? 'بدون نام'}</div>
<div style={{ fontSize: 12.5, color: 'var(--text-3)', direction: 'ltr', marginTop: 2 }}>{foundUser.mobile}</div>
</div>
</div>
)}
<p style={{ fontSize: 12.5, color: 'var(--text-3)', marginTop: 16, lineHeight: 1.7 }}>
بیمار باید قبلاً در سیستم ثبتنام کرده باشد. با شماره موبایل جستجو کنید، سپس پرونده ایجاد کنید.
</p>
</Modal>
</>
);
}
@@ -37,6 +37,29 @@ class PatientController extends BaseController
private readonly UserActiveContextRepository $contextRepo,
) {}
#[Route('/api/v1/patient/search-user', methods: ['GET'])]
public function searchUser(Request $request, #[CurrentUser] User $user): JsonResponse
{
[$entityType, $entityId] = $this->resolveEntity($user);
$this->assertPatientGate($entityType, $entityId);
$mobile = trim($request->query->get('mobile', ''));
if ($mobile === '') {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'mobile الزامی است', 422);
}
$patient = $this->userRepo->findByMobile($mobile);
if ($patient === null) {
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'کاربری با این شماره یافت نشد', 404);
}
return $this->success([
'uuid' => $patient->getUuid(),
'name' => $patient->getRealName(),
'mobile' => $patient->getMobileNumber(),
]);
}
#[Route('/api/v1/patients', methods: ['GET'])]
public function list(Request $request, #[CurrentUser] User $user): JsonResponse
{