feat(appointments): close the three remaining design gaps
1. شارژ کیف پول is now functional end-to-end. New owner-gated
POST /api/v1/patient/{uuid}/wallet/charge creates a manual credit
WalletTransaction (computed balance_after); the patient detail's wallet tab
gains a top-up modal (PriceInput + description) and supports ?tab= deep
links. The deposit sections of the create drawer, the edit page and the
replace modal link to it via WalletChargeLink (record resolved by mobile).
2. جایگزینی نوبت now matches appointments-replace.pdf: patient search-or-new,
بخش/سرویس/پرسنل selects prefilled from the appointment, deposit toggle +
amount + charge link, read-only original date/time, status pick and notes —
all through the general PATCH.
3. The confirmed-appointments table is paginated (20/page, client-side so the
schedule view and doctor-tab derivation keep the whole day), resetting on
date/doctor/filter changes. The page-local STATUS_META also adopts the
design labels plus following_up/salon for the schedule cards.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { screen, fireEvent } from '@testing-library/react';
|
||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { renderWithProviders } from '../test/utils';
|
||||
|
||||
@@ -109,6 +109,36 @@ describe('PatientDetailPage (پرونده تبدار)', () => {
|
||||
expect(await screen.findByText('شارژ')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens the wallet tab directly via ?tab=wallet and offers a top-up', async () => {
|
||||
renderWithProviders(
|
||||
<Routes><Route path="/admin/patients/:uuid" element={<PatientDetailPage />} /></Routes>,
|
||||
{ route: '/admin/patients/r1?tab=wallet' },
|
||||
);
|
||||
expect(await screen.findByText('موجودی کیف پول')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: /شارژ کیف پول/ }));
|
||||
expect(await screen.findByText('مبلغ شارژ (تومان)')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'ثبت شارژ' })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('posts the manual wallet charge', async () => {
|
||||
const post = api.post as ReturnType<typeof vi.fn>;
|
||||
post.mockResolvedValue({ success: true, data: { balance_rials: 800000, transaction: {} } });
|
||||
renderWithProviders(
|
||||
<Routes><Route path="/admin/patients/:uuid" element={<PatientDetailPage />} /></Routes>,
|
||||
{ route: '/admin/patients/r1?tab=wallet' },
|
||||
);
|
||||
await screen.findByText('موجودی کیف پول');
|
||||
fireEvent.click(screen.getByRole('button', { name: /شارژ کیف پول/ }));
|
||||
fireEvent.change(await screen.findByPlaceholderText('مثلاً: بیعانه نوبت'), { target: { value: 'بیعانه' } });
|
||||
// PriceInput displays toman; typing 30,000 toman = 300,000 rials
|
||||
const priceInput = screen.getByText('مبلغ شارژ (تومان)').parentElement!.querySelector('input')!;
|
||||
fireEvent.change(priceInput, { target: { value: '30000' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'ثبت شارژ' }));
|
||||
await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/patient/r1/wallet/charge', expect.objectContaining({
|
||||
description: 'بیعانه',
|
||||
})));
|
||||
});
|
||||
|
||||
it('renders the messages tab with a send box', async () => {
|
||||
renderDetail();
|
||||
await screen.findByText('ساغر صابری');
|
||||
|
||||
Reference in New Issue
Block a user