feat(appointments): row actions menu + info/move/transfer/replace modals — phase B
Frontend for the Figma عملیات menu on the confirmed-appointments table: - AppointmentActions composite: six-item row menu (ویرایش، ثبت سرویس، مشاهده، جا به جایی، انتقال به لیست رزرو، جایگزینی) plus the four modals it opens. ثبت سرویس and the info modal resolve the patient record via the patient-list search (mobile) to reuse the existing wallet endpoint and NewSessionPage. - Info modal mirrors appointments-info.pdf: start time, duration, phone, بخش/سرویس/پرسنل, wallet balance, status dropdown, مشاهده پرونده. - Move/transfer/replace modals PATCH the new general update endpoint with optimistic-lock version; transfer uses day-level midnight slots. - Status labels/transitions updated to the design set (ثبت شده/قطعی شده/ در حال پیگیری/سالن/ویزیت شده/لغو شده) in AppointmentStatusDropdown and StatusBadge; Appointment type gains the new workflow fields; the table gains سرویس/پرسنل/عملیات columns. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { renderWithProviders } from '../../test/utils';
|
||||
|
||||
vi.mock('../../lib/api', () => ({
|
||||
api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() },
|
||||
ApiError: class extends Error {},
|
||||
}));
|
||||
|
||||
import { api } from '../../lib/api';
|
||||
import AppointmentStatusDropdown from './AppointmentStatusDropdown';
|
||||
|
||||
const patch = api.patch as ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
patch.mockReset();
|
||||
patch.mockResolvedValue({ success: true, data: {} });
|
||||
});
|
||||
|
||||
describe('AppointmentStatusDropdown (وضعیت نوبت)', () => {
|
||||
it('shows the design label for the current status', () => {
|
||||
renderWithProviders(<AppointmentStatusDropdown uuid="a1" currentStatus="confirmed" version={1} queryKey={['x']} />);
|
||||
expect(screen.getByText('قطعی شده')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('offers the day-of workflow targets from confirmed', () => {
|
||||
renderWithProviders(<AppointmentStatusDropdown uuid="a1" currentStatus="confirmed" version={1} queryKey={['x']} />);
|
||||
fireEvent.click(screen.getByText('قطعی شده'));
|
||||
expect(screen.getByText('در حال پیگیری')).toBeInTheDocument();
|
||||
expect(screen.getByText('سالن')).toBeInTheDocument();
|
||||
expect(screen.getByText('ویزیت شده')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('patches the status endpoint on selection', async () => {
|
||||
renderWithProviders(<AppointmentStatusDropdown uuid="a1" currentStatus="salon" version={2} queryKey={['x']} />);
|
||||
fireEvent.click(screen.getByText('سالن'));
|
||||
fireEvent.click(screen.getByText('ویزیت شده'));
|
||||
await waitFor(() =>
|
||||
expect(patch).toHaveBeenCalledWith('/api/v1/appointment/a1/status', { status: 'completed', version: 2 }));
|
||||
});
|
||||
|
||||
it('disables terminal statuses', () => {
|
||||
renderWithProviders(<AppointmentStatusDropdown uuid="a1" currentStatus="completed" version={1} queryKey={['x']} />);
|
||||
expect(screen.getByText('ویزیت شده').closest('button')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
@@ -5,19 +5,25 @@ import { ChevronDownIcon } from '@heroicons/react/24/outline';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '../../lib/api';
|
||||
|
||||
const STATUS_META: Record<string, { label: string; color: string }> = {
|
||||
pending: { label: 'رزرو شده', color: '#3b82f6' },
|
||||
confirmed: { label: 'تأیید شده', color: '#22c55e' },
|
||||
completed: { label: 'تکمیل شده', color: '#16a34a' },
|
||||
cancelled_by_doctor: { label: 'لغو پزشک', color: '#ef4444' },
|
||||
cancelled_by_user: { label: 'لغو بیمار', color: '#ef4444' },
|
||||
// Labels follow the Figma نوبتها design (ثبت شده / قطعی شده / ویزیت شده …).
|
||||
export const STATUS_META: Record<string, { label: string; color: string }> = {
|
||||
pending: { label: 'ثبت شده', color: '#3b82f6' },
|
||||
confirmed: { label: 'قطعی شده', color: '#14b8a6' },
|
||||
following_up: { label: 'در حال پیگیری', color: '#f59e0b' },
|
||||
salon: { label: 'سالن', color: '#8b5cf6' },
|
||||
completed: { label: 'ویزیت شده', color: '#22c55e' },
|
||||
cancelled_by_doctor: { label: 'لغو شده', color: '#ef4444' },
|
||||
cancelled_by_user: { label: 'لغو توسط بیمار', color: '#ef4444' },
|
||||
no_show: { label: 'غیبت', color: '#9ca3af' },
|
||||
expired: { label: 'منقضی شده', color: '#9ca3af' },
|
||||
};
|
||||
|
||||
// Mirrors Appointment::ALLOWED_TRANSITIONS on the backend.
|
||||
const TRANSITIONS: Record<string, string[]> = {
|
||||
pending: ['confirmed', 'cancelled_by_doctor', 'cancelled_by_user', 'expired'],
|
||||
confirmed: ['completed', 'cancelled_by_doctor', 'cancelled_by_user', 'no_show'],
|
||||
pending: ['confirmed', 'following_up', 'cancelled_by_doctor', 'cancelled_by_user'],
|
||||
confirmed: ['completed', 'following_up', 'salon', 'cancelled_by_doctor', 'cancelled_by_user', 'no_show'],
|
||||
following_up: ['confirmed', 'salon', 'completed', 'cancelled_by_doctor', 'cancelled_by_user', 'no_show'],
|
||||
salon: ['completed', 'following_up', 'cancelled_by_doctor', 'cancelled_by_user', 'no_show'],
|
||||
};
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -4,11 +4,13 @@ import type { AppointmentStatus, PaymentStatus, SmsTemplateStatus, SettlementSta
|
||||
type BadgeColor = 'green' | 'amber' | 'red' | 'blue' | 'violet' | 'gray';
|
||||
|
||||
const appointmentMap: Record<AppointmentStatus, { color: BadgeColor; label: string }> = {
|
||||
pending: { color: 'blue', label: 'رزرو شده' },
|
||||
confirmed: { color: 'green', label: 'تأیید شده' },
|
||||
completed: { color: 'green', label: 'تکمیل شده' },
|
||||
cancelled_by_user: { color: 'red', label: 'لغو بیمار' },
|
||||
cancelled_by_doctor: { color: 'red', label: 'لغو پزشک' },
|
||||
pending: { color: 'blue', label: 'ثبت شده' },
|
||||
confirmed: { color: 'green', label: 'قطعی شده' },
|
||||
following_up: { color: 'amber', label: 'در حال پیگیری' },
|
||||
salon: { color: 'violet', label: 'سالن' },
|
||||
completed: { color: 'green', label: 'ویزیت شده' },
|
||||
cancelled_by_user: { color: 'red', label: 'لغو توسط بیمار' },
|
||||
cancelled_by_doctor: { color: 'red', label: 'لغو شده' },
|
||||
no_show: { color: 'gray', label: 'غیبت' },
|
||||
expired: { color: 'gray', label: 'منقضی' },
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user