feat(patient): add search user functionality by mobile number
This commit is contained in:
@@ -2305,6 +2305,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('آدرس حذف شد'); setDeletingAddrId(null);
|
toast.success('آدرس حذف شد'); setDeletingAddrId(null);
|
||||||
qc.invalidateQueries({ queryKey: ['doctor-detail', uuid] });
|
qc.invalidateQueries({ queryKey: ['doctor-detail', uuid] });
|
||||||
|
qc.invalidateQueries({ queryKey: ['available-locations', uuid] });
|
||||||
},
|
},
|
||||||
onError: (e: Error) => toast.error(e.message),
|
onError: (e: Error) => toast.error(e.message),
|
||||||
});
|
});
|
||||||
@@ -2764,7 +2765,10 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
|||||||
onClose={() => { setAddrModalOpen(false); setEditingAddr(null); }}
|
onClose={() => { setAddrModalOpen(false); setEditingAddr(null); }}
|
||||||
existing={editingAddr}
|
existing={editingAddr}
|
||||||
doctorUuid={uuid}
|
doctorUuid={uuid}
|
||||||
onSaved={() => qc.invalidateQueries({ queryKey: ['doctor-detail', uuid] })}
|
onSaved={() => {
|
||||||
|
qc.invalidateQueries({ queryKey: ['doctor-detail', uuid] });
|
||||||
|
qc.invalidateQueries({ queryKey: ['available-locations', uuid] });
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
MagnifyingGlassIcon, PlusIcon, ChevronRightIcon, PencilIcon, PhoneIcon,
|
MagnifyingGlassIcon, PlusIcon, ChevronRightIcon, PencilIcon, PhoneIcon,
|
||||||
FolderOpenIcon, UsersIcon,
|
FolderOpenIcon, UsersIcon, UserPlusIcon, CheckCircleIcon,
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
@@ -66,6 +66,12 @@ function MyPatientsPageInner() {
|
|||||||
const [sectionUuid, setSectionUuid] = useState('');
|
const [sectionUuid, setSectionUuid] = useState('');
|
||||||
const [itemUuid, setItemUuid] = 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 servicesTotal = selectedServices.reduce((sum, s) => sum + s.price_rials, 0);
|
||||||
const finalPrice = calcFinalPrice(Number(watchVisit), Number(watchBase), Number(watchSupp), servicesTotal);
|
const finalPrice = calcFinalPrice(Number(watchVisit), Number(watchBase), Number(watchSupp), servicesTotal);
|
||||||
|
|
||||||
@@ -126,6 +132,40 @@ function MyPatientsPageInner() {
|
|||||||
onError: (e: any) => toast.error(e.message),
|
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 = () => {
|
const handleAddService = () => {
|
||||||
if (!itemUuid) return;
|
if (!itemUuid) return;
|
||||||
const found = itemsData?.data?.find((i) => i.uuid === itemUuid);
|
const found = itemsData?.data?.find((i) => i.uuid === itemUuid);
|
||||||
@@ -192,7 +232,16 @@ function MyPatientsPageInner() {
|
|||||||
if (!selectedRecord) {
|
if (!selectedRecord) {
|
||||||
return (
|
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={{ marginBottom: 16 }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -234,6 +283,79 @@ function MyPatientsPageInner() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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,
|
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'])]
|
#[Route('/api/v1/patients', methods: ['GET'])]
|
||||||
public function list(Request $request, #[CurrentUser] User $user): JsonResponse
|
public function list(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user