From 665a210ef8308ea5cb9fbbd912c6a7faa26ebdac Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Mon, 13 Jul 2026 16:01:16 +0330 Subject: [PATCH] =?UTF-8?q?feat(patients):=20phase=20D=20=E2=80=94=20call-?= =?UTF-8?q?center=20tab=20(patient=20call=20log)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a record-scoped PatientCall entity (subject, summary, outcome success/missed, called_at, personnel) with its repository and three owner-gated endpoints on PatientController: GET /patient/{uuid}/calls — call log, newest first, optional ?outcome POST /patient/{uuid}/call — log a call (subject required) DELETE /patient/call/{uuid} — delete an entry Wire the previously-placeholder "کال سنتر" tab as a CallCenterTab: a register form (date/time/subject/summary + success/missed toggle, personnel taken from the logged-in user) beside a filterable call history (all / success / missed). With this every patient-detail tab is now backed by a real endpoint, so the generic Placeholder is no longer reachable. PatientCallTest covers create/list/ delete, the outcome filter, the invalid-outcome fallback, and ownership scoping. Co-Authored-By: Claude Opus 4.8 (1M context) --- assets/admin/pages/PatientDetailPage.test.tsx | 7 +- assets/admin/pages/PatientDetailPage.tsx | 112 ++++++++++++++++++ docs/api/patient.md | 27 +++++ migrations/Version20260713122600.php | 33 ++++++ src/Patient/Controller/PatientController.php | 73 ++++++++++++ src/Patient/Entity/PatientCall.php | 79 ++++++++++++ .../Repository/PatientCallRepository.php | 53 +++++++++ tests/Patient/PatientCallTest.php | 92 ++++++++++++++ 8 files changed, 474 insertions(+), 2 deletions(-) create mode 100644 migrations/Version20260713122600.php create mode 100644 src/Patient/Entity/PatientCall.php create mode 100644 src/Patient/Repository/PatientCallRepository.php create mode 100644 tests/Patient/PatientCallTest.php diff --git a/assets/admin/pages/PatientDetailPage.test.tsx b/assets/admin/pages/PatientDetailPage.test.tsx index 5467e88a..ffca079d 100644 --- a/assets/admin/pages/PatientDetailPage.test.tsx +++ b/assets/admin/pages/PatientDetailPage.test.tsx @@ -59,11 +59,14 @@ describe('PatientDetailPage (پرونده تب‌دار)', () => { expect(screen.getByText('اینستاگرام')).toBeInTheDocument(); }); - it('shows a placeholder for not-yet-built tabs', async () => { + it('renders the call-center tab with a register form and history', async () => { renderDetail(); await screen.findByText('ساغر صابری'); fireEvent.click(screen.getByText('کال سنتر')); - expect(screen.getByText(/به‌زودی تکمیل می‌شود/)).toBeInTheDocument(); + expect(await screen.findByText('ثبت تماس جدید')).toBeInTheDocument(); + expect(screen.getByText('تاریخچه تماس‌ها')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('موضوع تماس')).toBeInTheDocument(); + expect(await screen.findByText('تماسی ثبت نشده است.')).toBeInTheDocument(); }); it('renders the attachments tab with an upload button', async () => { diff --git a/assets/admin/pages/PatientDetailPage.tsx b/assets/admin/pages/PatientDetailPage.tsx index 760ed093..e68c39e2 100644 --- a/assets/admin/pages/PatientDetailPage.tsx +++ b/assets/admin/pages/PatientDetailPage.tsx @@ -146,6 +146,8 @@ export default function PatientDetailPage() { row={(p) => ({ title: formatRial(p.amount_rials), meta: [p.created_at ? formatDate(p.created_at) : '', p.gateway].filter(Boolean).join(' · '), badge: PAYMENT_STATUS[p.status] || p.status })} /> ) : tab === 'wallet' ? ( + ) : tab === 'callcenter' ? ( + ) : tab === 'attach' ? ( ) : tab === 'records' ? ( @@ -403,6 +405,116 @@ function MessagesTab({ uuid }: { uuid: string }) { ); } +interface Call { uuid: string; subject: string; summary?: string | null; outcome: string; called_at: number; personnel?: string | null } + +/** کال سنتر — patient call log: register a call + filterable history (all / success / missed). */ +function CallCenterTab({ uuid }: { uuid: string }) { + const qc = useQueryClient(); + const userName = useAuthStore((s) => s.userName); + const [filter, setFilter] = useState<'all' | 'success' | 'missed'>('all'); + const [date, setDate] = useState(''); + const [time, setTime] = useState(''); + const [subject, setSubject] = useState(''); + const [summary, setSummary] = useState(''); + const [outcome, setOutcome] = useState<'success' | 'missed'>('success'); + + const { data, isLoading } = useQuery>({ + queryKey: ['patient-calls', uuid], + queryFn: () => api.get(`/api/v1/patient/${uuid}/calls`), + }); + const calls = data?.data ?? []; + const shown = filter === 'all' ? calls : calls.filter((c) => c.outcome === filter); + const successCount = calls.filter((c) => c.outcome === 'success').length; + const missedCount = calls.filter((c) => c.outcome === 'missed').length; + const invalidate = () => qc.invalidateQueries({ queryKey: ['patient-calls', uuid] }); + + const create = useMutation({ + mutationFn: () => { + const iso = date ? `${date}T${time || '00:00'}` : null; + const calledAt = iso ? Math.floor(new Date(iso).getTime() / 1000) : Math.floor(Date.now() / 1000); + return api.post(`/api/v1/patient/${uuid}/call`, { subject, summary, outcome, called_at: calledAt, personnel: userName }); + }, + onSuccess: () => { invalidate(); setDate(''); setTime(''); setSubject(''); setSummary(''); setOutcome('success'); toast.success('تماس ثبت شد'); }, + onError: (e: any) => toast.error(e.message), + }); + const del = useMutation({ + mutationFn: (u: string) => api.delete(`/api/v1/patient/call/${u}`), + onSuccess: () => { invalidate(); toast.success('تماس حذف شد'); }, + onError: (e: any) => toast.error(e.message), + }); + + const chip = (key: 'all' | 'success' | 'missed', label: string) => ( + + ); + + return ( +
+ {/* register form */} +
+
ثبت تماس جدید
+ +
+ +
setTime(e.target.value)} dir="ltr" />
+ +
setSubject(e.target.value)} placeholder="موضوع تماس" />
+ +