refactor: update pricing input handling to use PriceInput component
- Replaced raw input fields for pricing with PriceInput component across various forms and modals to ensure consistent formatting and accessibility. - Updated tests to reflect changes in pricing input handling, ensuring values are displayed in toman with proper formatting. - Enhanced accessibility by adding aria-labels and aria-invalid attributes to PriceInput components. - Adjusted UI elements to improve layout and user experience, particularly in forms related to service items and scheduling. - Changed labels from "نمایش در نوبتدهی" to "نمایش در نوبتدهی آنلاین" for clarity.
This commit is contained in:
@@ -9,6 +9,9 @@ import ResourceServicesPanel from './ResourceServicesPanel';
|
||||
|
||||
const resource = { uuid: 'r-1', name: 'دستگاه شمارهٔ ۲' } as never;
|
||||
|
||||
/** PriceInput مبلغ را با جداکنندهٔ fa-IR نشان میدهد، نه رقمِ خام. */
|
||||
const fa = (n: number) => new Intl.NumberFormat('fa-IR').format(n);
|
||||
|
||||
const services = [
|
||||
{ uuid: 's-1', name: 'لیزر پا', duration_minutes: 30, price_rials: 8_000_000 },
|
||||
{ uuid: 's-2', name: 'لیزر دست', duration_minutes: 20, price_rials: 5_000_000 },
|
||||
@@ -57,12 +60,17 @@ describe('ResourceServicesPanel', () => {
|
||||
expect(price).toHaveValue('');
|
||||
});
|
||||
|
||||
it('override را ذخیره میکند', async () => {
|
||||
it('قیمت به تومان گرفته میشود و به ریال ذخیره میشود', async () => {
|
||||
const onSave = vi.fn();
|
||||
renderWithProviders(<ResourceServicesPanel {...props({ onSave })} />);
|
||||
|
||||
// برچسب و placeholder هر دو توماناند؛ پیش از این برچسب «ریال» بود و ورودی
|
||||
// کاربر ده برابر کمتر از چیزی میشد که فکر میکرد وارد کرده.
|
||||
expect(screen.getByText('قیمت (تومان)')).toBeInTheDocument();
|
||||
expect(screen.queryByText('قیمت (ریال)')).not.toBeInTheDocument();
|
||||
|
||||
const price = screen.getByPlaceholderText(/پیشفرض سرویس/);
|
||||
await userEvent.type(price, '9500000');
|
||||
await userEvent.type(price, '950000');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'ذخیره' }));
|
||||
|
||||
expect(onSave).toHaveBeenCalledWith([
|
||||
@@ -70,6 +78,33 @@ describe('ResourceServicesPanel', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('قیمتِ override شده به تومان در فیلد مینشیند', () => {
|
||||
renderWithProviders(<ResourceServicesPanel {...props({
|
||||
offerings: [{ ...offerings[0], price_rials: 9_500_000, price_source: 'resource_option' }] as never,
|
||||
})} />);
|
||||
|
||||
expect(screen.getByDisplayValue(fa(950_000))).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('سرویس تازه با مدت و قیمت پیشفرض خودش پر میشود', async () => {
|
||||
const onSave = vi.fn();
|
||||
renderWithProviders(<ResourceServicesPanel {...props({ onSave })} />);
|
||||
|
||||
await userEvent.click(screen.getByText('یک سرویس انتخاب کنید'));
|
||||
await userEvent.click(await screen.findByText('لیزر دست'));
|
||||
|
||||
// ۵٬۰۰۰٬۰۰۰ ریال = ۵۰۰٬۰۰۰ تومان
|
||||
expect(screen.getByDisplayValue(fa(500_000))).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('20')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'ذخیره' }));
|
||||
|
||||
expect(onSave).toHaveBeenCalledWith([
|
||||
{ service_uuid: 's-1', duration_minutes: '15', price_rials: '', active: true },
|
||||
{ service_uuid: 's-2', duration_minutes: '20', price_rials: '5000000', active: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it('پاککردن مقدار یعنی بازگشت به ارث، نه صفر', async () => {
|
||||
const onSave = vi.fn();
|
||||
renderWithProviders(<ResourceServicesPanel {...props({ onSave })} />);
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PriceInput from '../ui/PriceInput';
|
||||
import SearchableSelect from '../ui/SearchableSelect';
|
||||
import Switch from '../ui/Switch';
|
||||
import { formatRial } from '../../lib/utils';
|
||||
import { formatRial, rialToToman, tomanToRial } from '../../lib/utils';
|
||||
import type { ClinicResource, ResourceServiceOffering, ServiceItemOption } from '../../types';
|
||||
|
||||
type Line = {
|
||||
service_uuid: string;
|
||||
service_name: string;
|
||||
duration_minutes: string;
|
||||
price_rials: string;
|
||||
/** مقدارِ داخل فیلد به **تومان** است؛ تبدیل به ریال فقط موقع ذخیره انجام میشود. */
|
||||
price_tomans: string;
|
||||
active: boolean;
|
||||
effective_duration_minutes: number | null;
|
||||
effective_price_rials: number;
|
||||
@@ -63,7 +65,9 @@ export default function ResourceServicesPanel({
|
||||
service_uuid: o.service_uuid,
|
||||
service_name: o.service_name,
|
||||
duration_minutes: o.duration_minutes === null ? '' : String(o.duration_minutes),
|
||||
price_rials: o.price_rials === null ? '' : String(o.price_rials),
|
||||
// ورودی به تومان است تا با placeholder و بقیهٔ پنل یکی باشد؛ پیش از این
|
||||
// برچسب «ریال» بود و placeholder تومان — یعنی ده برابر اختلاف در یک فیلد.
|
||||
price_tomans: o.price_rials === null ? '' : String(rialToToman(o.price_rials)),
|
||||
active: o.active,
|
||||
effective_duration_minutes: o.effective_duration_minutes,
|
||||
effective_price_rials: o.effective_price_rials,
|
||||
@@ -142,12 +146,12 @@ export default function ResourceServicesPanel({
|
||||
</div>
|
||||
|
||||
<div className="field-block" style={{ flex: 1 }}>
|
||||
<label style={{ fontSize: 12, color: 'var(--text-2)' }}>قیمت (ریال)</label>
|
||||
<input
|
||||
<label style={{ fontSize: 12, color: 'var(--text-2)' }}>قیمت (تومان)</label>
|
||||
<PriceInput
|
||||
className="field"
|
||||
inputMode="numeric"
|
||||
value={line.price_rials}
|
||||
onChange={(e) => patch(index, { price_rials: e.target.value })}
|
||||
ariaLabel={`قیمت ${line.service_name} (تومان)`}
|
||||
value={line.price_tomans === '' ? '' : Number(line.price_tomans)}
|
||||
onChange={(v) => patch(index, { price_tomans: v === 0 ? '' : String(v) })}
|
||||
placeholder={`${formatRial(line.effective_price_rials)} — ${SOURCE_LABELS[line.price_source] ?? '—'}`}
|
||||
/>
|
||||
</div>
|
||||
@@ -172,8 +176,11 @@ export default function ResourceServicesPanel({
|
||||
{
|
||||
service_uuid: picked.uuid,
|
||||
service_name: picked.name,
|
||||
duration_minutes: '',
|
||||
price_rials: '',
|
||||
// سرویسِ تازه با پیشفرض خودش پر میشود تا کاربر عددی جلوی چشمش
|
||||
// باشد و لازم نباشد از placeholder رونویسی کند. پاککردنش همان
|
||||
// «ارث از سرویس» را برمیگرداند؛ ردیفهای موجود دست نمیخورند.
|
||||
duration_minutes: picked.duration_minutes == null ? '' : String(picked.duration_minutes),
|
||||
price_tomans: picked.price_rials == null ? '' : String(rialToToman(picked.price_rials)),
|
||||
active: true,
|
||||
effective_duration_minutes: picked.duration_minutes ?? null,
|
||||
effective_price_rials: picked.price_rials ?? 0,
|
||||
@@ -189,8 +196,9 @@ export default function ResourceServicesPanel({
|
||||
)}
|
||||
|
||||
<p style={{ fontSize: 12, color: 'var(--text-3)', margin: 0 }}>
|
||||
خالی گذاشتن مدت یا قیمت یعنی همان مقدارِ سطح بالاتر استفاده شود. ذخیره کل فهرست را
|
||||
جایگزین میکند؛ سرویسی که اینجا نباشد از این منبع برداشته میشود.
|
||||
سرویس تازه با مدت و قیمت پیشفرض خودش پر میشود؛ میتوانید عوضش کنید. خالی گذاشتن
|
||||
مدت یا قیمت یعنی همان مقدارِ سطح بالاتر استفاده شود. ذخیره کل فهرست را جایگزین
|
||||
میکند؛ سرویسی که اینجا نباشد از این منبع برداشته میشود.
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
||||
@@ -203,12 +211,16 @@ export default function ResourceServicesPanel({
|
||||
disabled={saving}
|
||||
onClick={() =>
|
||||
onSave(
|
||||
lines.map((l) => ({
|
||||
service_uuid: l.service_uuid,
|
||||
duration_minutes: l.duration_minutes.trim(),
|
||||
price_rials: l.price_rials.trim(),
|
||||
active: l.active,
|
||||
})),
|
||||
lines.map((l) => {
|
||||
const toman = l.price_tomans.trim();
|
||||
return {
|
||||
service_uuid: l.service_uuid,
|
||||
duration_minutes: l.duration_minutes.trim(),
|
||||
// قرارداد سرور ریال است؛ رشتهٔ خالی یعنی «ارث» و نباید صفر شود.
|
||||
price_rials: toman === '' ? '' : String(tomanToRial(Number(toman))),
|
||||
active: l.active,
|
||||
};
|
||||
}),
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user