feat: refactor ResourceExceptionsPanel and related hooks; remove unused availability logic and simplify resource detail management
This commit is contained in:
@@ -15,6 +15,7 @@ import { api } from '../lib/api';
|
||||
import ResourceDetailPage from './ResourceDetailPage';
|
||||
|
||||
const get = api.get as ReturnType<typeof vi.fn>;
|
||||
const patch = api.patch as ReturnType<typeof vi.fn>;
|
||||
|
||||
const resource = {
|
||||
uuid: 'r1',
|
||||
@@ -41,7 +42,7 @@ const resource = {
|
||||
const emptyDays = (): Record<string, unknown[]> =>
|
||||
Object.fromEntries(Array.from({ length: 7 }, (_, d) => [String(d), [] as unknown[]]));
|
||||
|
||||
function mockApi(days: Record<string, unknown[]> = emptyDays(), availabilityDays: unknown[] = []) {
|
||||
function mockApi(days: Record<string, unknown[]> = emptyDays()) {
|
||||
get.mockImplementation((path: string) => {
|
||||
if (path === '/api/v1/resource/r1') return Promise.resolve({ success: true, data: resource });
|
||||
if (path.endsWith('/calendar')) {
|
||||
@@ -50,12 +51,6 @@ function mockApi(days: Record<string, unknown[]> = emptyDays(), availabilityDays
|
||||
data: { resource_uuid: 'r1', timezone: 'Asia/Tehran', defined: true, days },
|
||||
});
|
||||
}
|
||||
if (path.includes('/availability')) {
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
data: { resource_uuid: 'r1', timezone: 'Asia/Tehran', days: availabilityDays },
|
||||
});
|
||||
}
|
||||
if (path.includes('/service-categories/tree')) {
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
@@ -107,40 +102,6 @@ describe('ResourceDetailPage', () => {
|
||||
expect(screen.getByDisplayValue('17:00')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
/**
|
||||
* دلیلِ خالی بودن روز باید فارسی نشان داده شود؛ نشان دادن کلید خام سرور
|
||||
* («outside_branch_hours») به کاربر یعنی پیام بیمعنا.
|
||||
*/
|
||||
it('دلیل خالی بودن روز را فارسی میکند', async () => {
|
||||
mockApi(emptyDays(), [
|
||||
{ date: 1785529800, day_of_week: 0, intervals: [], total_minutes: 0, reasons: ['national_holiday'] },
|
||||
{ date: 1785616200, day_of_week: 1, intervals: [], total_minutes: 0, reasons: ['no_shift'] },
|
||||
{ date: 1785702600, day_of_week: 2, intervals: [], total_minutes: 0, reasons: ['exception'] },
|
||||
]);
|
||||
renderPage('/admin/resources/r1?tab=exceptions');
|
||||
|
||||
await waitFor(() => expect(screen.getByText('تعطیل رسمی')).toBeInTheDocument());
|
||||
expect(screen.getByText('شیفتی تعریف نشده')).toBeInTheDocument();
|
||||
expect(screen.getByText('مرخصی یا سرویس')).toBeInTheDocument();
|
||||
expect(screen.queryByText('no_shift')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
/**
|
||||
* وقتی هیچ روزی شیفت ندارد، ۱۴ سطرِ یکسان نویز است و با یک حالت خالی جمع میشود.
|
||||
* شرط باید تنگ بماند: تست بالا ثابت میکند دلیلِ غیر از `no_shift` سطرها را نگه میدارد.
|
||||
*/
|
||||
it('پیشنمایشِ کاملاً تعریفنشده را در یک حالت خالی جمع میکند', async () => {
|
||||
mockApi(emptyDays(), [
|
||||
{ date: 1785529800, day_of_week: 0, intervals: [], total_minutes: 0, reasons: ['no_shift'] },
|
||||
{ date: 1785616200, day_of_week: 1, intervals: [], total_minutes: 0, reasons: ['no_shift'] },
|
||||
]);
|
||||
renderPage('/admin/resources/r1?tab=exceptions');
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/در دو هفتهٔ آینده هیچ ساعتی باز نیست/)).toBeInTheDocument());
|
||||
expect(screen.queryByText('شیفتی تعریف نشده')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
/** نوار ذخیره فقط وقتی ظاهر میشود که چیزی تغییر کرده باشد — خودش نشانهٔ dirty است. */
|
||||
it('نوار ذخیره تا وقتی تغییری نباشد نمیآید', async () => {
|
||||
const user = userEvent.setup();
|
||||
@@ -227,14 +188,6 @@ describe('ResourceDetailPage', () => {
|
||||
expect(screen.queryByRole('button', { name: /کپی/ })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
/** پیشنمایش نباید «وقت قابل رزرو» خوانده شود — نوبتها هنوز کسر نشدهاند. */
|
||||
it('پیشنمایش را خام معرفی میکند', async () => {
|
||||
mockApi();
|
||||
renderPage('/admin/resources/r1?tab=exceptions');
|
||||
|
||||
await waitFor(() => expect(screen.getByText(/نوبتهای ثبتشده هنوز از آن کسر نشدهاند/)).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('تب دستهبندی فقط انتخاب میدهد، نه ساخت', async () => {
|
||||
mockApi();
|
||||
const user = userEvent.setup();
|
||||
@@ -248,6 +201,16 @@ describe('ResourceDetailPage', () => {
|
||||
expect(screen.getByText('تنظیمات ← دستهبندیها')).toHaveAttribute('href', '/admin/service-categories');
|
||||
});
|
||||
|
||||
/** تب استثناها فقط دو ورودی دارد: تعطیلات رسمی، و مرخصی و سرویس. */
|
||||
it('تب تعطیلات پیشنمایش دو هفته را ندارد', async () => {
|
||||
mockApi();
|
||||
renderPage('/admin/resources/r1?tab=exceptions');
|
||||
|
||||
await waitFor(() => expect(screen.getByText('مرخصی و سرویس')).toBeInTheDocument());
|
||||
expect(screen.queryByText('پیشنمایش دو هفته')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/نوبتهای ثبتشده هنوز از آن کسر نشدهاند/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
/**
|
||||
* مسدودسازی موردی از مودالِ جدول به تبِ خودِ منبع آمد و «غیرفعالسازی موقت» نام گرفت.
|
||||
* متن تب باید تفاوتش با استثنای تقویم را بگوید، وگرنه اپراتور تعطیلیِ یک بعدازظهر را
|
||||
@@ -267,8 +230,11 @@ describe('ResourceDetailPage', () => {
|
||||
expect(screen.getByRole('heading', { name: 'غیرفعالسازی موقت' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
/** فعال/غیرفعال و حذف از ردیف جدول به تب اطلاعات آمدند. */
|
||||
it('تب اطلاعات، کلید فعال/غیرفعال و حذف منبع را دارد', async () => {
|
||||
/**
|
||||
* حذفِ منبع عمداً در رابط کاربری نیست: منبع به نوبتهای ثبتشده گره خورده و راهِ
|
||||
* درستِ «دیگر لازمش ندارم» غیرفعال کردن است.
|
||||
*/
|
||||
it('تب اطلاعات کلید فعال/غیرفعال دارد و حذف منبع را نه', async () => {
|
||||
mockApi();
|
||||
renderPage();
|
||||
|
||||
@@ -276,19 +242,51 @@ describe('ResourceDetailPage', () => {
|
||||
|
||||
// منبعِ نمونه فعال است، پس اقدام «غیرفعال کردن» پیشنهاد میشود.
|
||||
expect(screen.getByRole('button', { name: 'غیرفعال کردن' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'حذف' })).toBeInTheDocument();
|
||||
expect(screen.getByText(/اگر فقط موقتاً لازمش ندارید/)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'حذف' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/حذف منبع/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
/** حذف بدون تأیید انجام نمیشود — عملی است که برگشت ندارد. */
|
||||
it('حذف اول تأیید میگیرد', async () => {
|
||||
/** غیرفعال کردن منبع را از نوبتدهی بیرون میبرد، پس بیتأیید انجام نمیشود. */
|
||||
it('غیرفعال کردن اول تأیید میگیرد و بدون تأیید درخواستی نمیفرستد', async () => {
|
||||
mockApi();
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => expect(screen.getByText('شعبهٔ مرکزی')).toBeInTheDocument());
|
||||
await user.click(screen.getByRole('button', { name: 'حذف' }));
|
||||
await user.click(screen.getByRole('button', { name: 'غیرفعال کردن' }));
|
||||
|
||||
expect(await screen.findByText(/مطمئن هستید؟ این کار برگشتپذیر نیست/)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/در جستجوی وقت و رزرو نوبت ظاهر نمیشود/)).toBeInTheDocument();
|
||||
expect(patch).not.toHaveBeenCalled();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'غیرفعال کن' }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(patch).toHaveBeenCalledWith('/api/v1/resource/r1', { active: false }));
|
||||
});
|
||||
|
||||
/**
|
||||
* رگرسیون: جهش فقط کلیدِ فهرست (`resources`) را باطل میکرد، نه
|
||||
* `resource-detail` را — پس وضعیت روی همین صفحه تا رفرشِ دستی کهنه میماند.
|
||||
*/
|
||||
it('وضعیت بعد از غیرفعال کردن بدون رفرش بهروز میشود', async () => {
|
||||
mockApi();
|
||||
const user = userEvent.setup();
|
||||
patch.mockResolvedValue({ success: true, data: { ...resource, active: false } });
|
||||
renderPage();
|
||||
|
||||
await waitFor(() => expect(screen.getByText('فعال')).toBeInTheDocument());
|
||||
|
||||
// پاسخِ بعدیِ سرور برای همین منبع، غیرفعال است.
|
||||
get.mockImplementation((path: string) =>
|
||||
path === '/api/v1/resource/r1'
|
||||
? Promise.resolve({ success: true, data: { ...resource, active: false } })
|
||||
: Promise.resolve({ success: true, data: [] }));
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'غیرفعال کردن' }));
|
||||
await user.click(await screen.findByRole('button', { name: 'غیرفعال کن' }));
|
||||
|
||||
// بدون باطلکردنِ `resource-detail`، این همیشه «فعال» میماند.
|
||||
expect(await screen.findByText('غیرفعال')).toBeInTheDocument();
|
||||
expect(await screen.findByRole('button', { name: 'فعال کردن' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user