feat: port secretaries tab from tauri to admin my-secretaries page
- Redesign MySecretariesPage pixel-perfect to clinic-pro-tauri (active/previous tabs, desktop table, mobile cards, add/edit/view modal with permission accordions, deactivate confirm) - Permission sections based on existing admin pages (appointments, patients, payments, insurances, addresses, clinic_info) - Extend DoctorSecretary with national_code + address columns (+migration); wire create/update in SecretaryController; add patients/payments to DEFAULT_PERMISSIONS - Extend Secretary/SecretaryPermissions types; update admin SecretariesPage - Backend + frontend tests; update docs/api/secretary.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { screen, fireEvent } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../test/utils";
|
||||
|
||||
vi.mock("sonner", () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
||||
vi.mock("../lib/api", () => ({
|
||||
api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() },
|
||||
ApiError: class extends Error {},
|
||||
}));
|
||||
vi.mock("../stores/authStore", () => ({
|
||||
useAuthStore: () => ({ doctorUuid: "doc-1", dbUuid: "doc-1", primaryRole: "doctor" }),
|
||||
}));
|
||||
vi.mock("../hooks/useSubscription", () => ({
|
||||
useSubscription: () => ({ maxSecretaries: 5 }),
|
||||
}));
|
||||
|
||||
import { api } from "../lib/api";
|
||||
import MySecretariesPage from "./MySecretariesPage";
|
||||
|
||||
const get = api.get as ReturnType<typeof vi.fn>;
|
||||
|
||||
const fullPerms = {
|
||||
appointments: { view: true, create: false, cancel: false, update_status: false },
|
||||
patients: { view: false, create: false, update: false, delete: false },
|
||||
payments: { view: false, create: false, update: false, delete: false },
|
||||
insurances: { view: false, create: false, update: false, delete: false },
|
||||
addresses: { view: false, create: false, update: false, delete: false },
|
||||
clinic_info: { view: false, update: false },
|
||||
};
|
||||
|
||||
const activeSecretary = {
|
||||
uuid: "sec-1",
|
||||
user_name: "سارا احمدی",
|
||||
mobile_number: "09121234567",
|
||||
doctor_name: "دکتر تست",
|
||||
doctor_uuid: "doc-1",
|
||||
is_active: true,
|
||||
national_code: "1234567890",
|
||||
address: "یزد",
|
||||
permissions: fullPerms,
|
||||
created_at: 1700000000,
|
||||
};
|
||||
|
||||
const previousSecretary = {
|
||||
...activeSecretary,
|
||||
uuid: "sec-2",
|
||||
user_name: "مینا رضایی",
|
||||
mobile_number: "09129876543",
|
||||
is_active: false,
|
||||
national_code: "9999999999",
|
||||
};
|
||||
|
||||
function mockData(rows = [activeSecretary, previousSecretary]) {
|
||||
get.mockImplementation((url: string) => {
|
||||
if (url.includes("/secretaries/")) return Promise.resolve({ success: true, data: rows });
|
||||
return Promise.resolve({ success: true, data: [] });
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
get.mockReset();
|
||||
mockData();
|
||||
});
|
||||
|
||||
describe("MySecretariesPage", () => {
|
||||
it("renders the title and both tabs", async () => {
|
||||
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
|
||||
expect(await screen.findByText("لیست منشی ها")).toBeInTheDocument();
|
||||
expect(screen.getByText("منشی های فعلی")).toBeInTheDocument();
|
||||
expect(screen.getByText("منشی های قبلی")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows active secretaries with national code on the default tab", async () => {
|
||||
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
|
||||
expect(await screen.findAllByText("سارا احمدی")).not.toHaveLength(0);
|
||||
expect(screen.getAllByText("1234567890").length).toBeGreaterThan(0);
|
||||
// inactive secretary is hidden on the active tab
|
||||
expect(screen.queryByText("مینا رضایی")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("switches to the previous tab and lists inactive secretaries", async () => {
|
||||
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
|
||||
await screen.findAllByText("سارا احمدی");
|
||||
|
||||
fireEvent.click(screen.getByText("منشی های قبلی"));
|
||||
|
||||
expect(await screen.findAllByText("مینا رضایی")).not.toHaveLength(0);
|
||||
expect(screen.queryByText("سارا احمدی")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows an empty state when there are no active secretaries", async () => {
|
||||
mockData([previousSecretary]);
|
||||
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
|
||||
expect(await screen.findByText("هنوز منشی فعالی اضافه نشده است")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens the add modal with permission sections based on existing pages", async () => {
|
||||
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
|
||||
await screen.findAllByText("سارا احمدی");
|
||||
|
||||
fireEvent.click(screen.getByText("اضافه کردن منشی"));
|
||||
|
||||
expect(await screen.findByText("اضافه کردن منشی جدید")).toBeInTheDocument();
|
||||
expect(screen.getByText("مجوزهای دسترسی")).toBeInTheDocument();
|
||||
expect(screen.getByText("مدیریت نوبتها")).toBeInTheDocument();
|
||||
expect(screen.getByText("پرونده بیماران")).toBeInTheDocument();
|
||||
expect(screen.getByText("مدیریت پرداختها")).toBeInTheDocument();
|
||||
expect(screen.getByText("مدیریت بیمهها")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,9 +14,11 @@ import Modal from '../components/ui/Modal';
|
||||
|
||||
const DEFAULT_PERMISSIONS: SecretaryPermissions = {
|
||||
appointments: { view: true, create: false, cancel: false, update_status: false },
|
||||
patients: { view: true, create: false, update: false, delete: false },
|
||||
payments: { view: true, create: false, update: false, delete: false },
|
||||
insurances: { view: true, create: false, update: false, delete: false },
|
||||
addresses: { view: true, create: false, update: false, delete: false },
|
||||
clinic_info: { view: true, update: false },
|
||||
insurances: { view: true, create: false, update: false, delete: false },
|
||||
};
|
||||
|
||||
type PermSection = keyof SecretaryPermissions;
|
||||
@@ -31,6 +33,24 @@ const PERMISSION_LABELS: Record<PermSection, { label: string; actions: { key: st
|
||||
{ key: 'update_status', label: 'تغییر وضعیت' },
|
||||
],
|
||||
},
|
||||
patients: {
|
||||
label: 'پرونده بیماران',
|
||||
actions: [
|
||||
{ key: 'view', label: 'مشاهده' },
|
||||
{ key: 'create', label: 'ایجاد' },
|
||||
{ key: 'update', label: 'ویرایش' },
|
||||
{ key: 'delete', label: 'حذف' },
|
||||
],
|
||||
},
|
||||
payments: {
|
||||
label: 'پرداختها',
|
||||
actions: [
|
||||
{ key: 'view', label: 'مشاهده' },
|
||||
{ key: 'create', label: 'ایجاد' },
|
||||
{ key: 'update', label: 'ویرایش' },
|
||||
{ key: 'delete', label: 'حذف' },
|
||||
],
|
||||
},
|
||||
addresses: {
|
||||
label: 'آدرسها',
|
||||
actions: [
|
||||
|
||||
@@ -330,6 +330,8 @@ export interface Secretary {
|
||||
doctor_name: string;
|
||||
doctor_uuid: string;
|
||||
is_active: boolean;
|
||||
national_code?: string | null;
|
||||
address?: string | null;
|
||||
permissions: SecretaryPermissions;
|
||||
created_at: string;
|
||||
}
|
||||
@@ -341,6 +343,24 @@ export interface SecretaryPermissions {
|
||||
cancel: boolean;
|
||||
update_status: boolean;
|
||||
};
|
||||
patients: {
|
||||
view: boolean;
|
||||
create: boolean;
|
||||
update: boolean;
|
||||
delete: boolean;
|
||||
};
|
||||
payments: {
|
||||
view: boolean;
|
||||
create: boolean;
|
||||
update: boolean;
|
||||
delete: boolean;
|
||||
};
|
||||
insurances: {
|
||||
view: boolean;
|
||||
create: boolean;
|
||||
update: boolean;
|
||||
delete: boolean;
|
||||
};
|
||||
addresses: {
|
||||
view: boolean;
|
||||
create: boolean;
|
||||
@@ -351,12 +371,6 @@ export interface SecretaryPermissions {
|
||||
view: boolean;
|
||||
update: boolean;
|
||||
};
|
||||
insurances: {
|
||||
view: boolean;
|
||||
create: boolean;
|
||||
update: boolean;
|
||||
delete: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Specialty {
|
||||
|
||||
Reference in New Issue
Block a user