- Added seed_realistic_data.php to clean existing data and populate the database with realistic entries for doctors, clinics, and secretaries. - Created a structured approach to generate 100 doctors per city with diverse specialties and services. - Implemented database cleanup routines to ensure a fresh start for data seeding. - Enhanced the DoctorSecretaryRepository with improved comments for clarity.
812 lines
33 KiB
TypeScript
812 lines
33 KiB
TypeScript
import {
|
|
CheckCircleIcon,
|
|
IdentificationIcon,
|
|
NoSymbolIcon,
|
|
PencilIcon,
|
|
PhoneIcon,
|
|
PlusIcon,
|
|
} from "@heroicons/react/24/outline";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { useState } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { Link } from "react-router-dom";
|
|
import { toast } from "sonner";
|
|
import { z } from "zod";
|
|
import ConfirmDialog from "../components/ui/ConfirmDialog";
|
|
import DataTable, { type Column } from "../components/ui/DataTable";
|
|
import Modal from "../components/ui/Modal";
|
|
import PageHeader from "../components/ui/PageHeader";
|
|
import { ActiveBadge } from "../components/ui/StatusBadge";
|
|
import { useSubscription } from "../hooks/useSubscription";
|
|
import type { ApiResponse } from "../lib/api";
|
|
import { api } from "../lib/api";
|
|
import { formatDate, maskMobile } from "../lib/utils";
|
|
import { useAuthStore } from "../stores/authStore";
|
|
import type { Secretary, SecretaryPermissions } from "../types";
|
|
|
|
// ── Default permissions & labels ──────────────────────────────────────────
|
|
|
|
const DEFAULT_PERMISSIONS: SecretaryPermissions = {
|
|
appointments: {
|
|
view: true,
|
|
create: false,
|
|
cancel: false,
|
|
update_status: 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;
|
|
|
|
const PERMISSION_LABELS: Record<
|
|
PermSection,
|
|
{ label: string; actions: { key: string; label: string }[] }
|
|
> = {
|
|
appointments: {
|
|
label: "نوبتها",
|
|
actions: [
|
|
{ key: "view", label: "مشاهده" },
|
|
{ key: "create", label: "ایجاد" },
|
|
{ key: "cancel", label: "لغو" },
|
|
{ key: "update_status", label: "تغییر وضعیت" },
|
|
],
|
|
},
|
|
addresses: {
|
|
label: "آدرسها",
|
|
actions: [
|
|
{ key: "view", label: "مشاهده" },
|
|
{ key: "create", label: "ایجاد" },
|
|
{ key: "update", label: "ویرایش" },
|
|
{ key: "delete", label: "حذف" },
|
|
],
|
|
},
|
|
clinic_info: {
|
|
label: "اطلاعات کلینیک",
|
|
actions: [
|
|
{ key: "view", label: "مشاهده" },
|
|
{ key: "update", label: "ویرایش" },
|
|
],
|
|
},
|
|
insurances: {
|
|
label: "بیمهها",
|
|
actions: [
|
|
{ key: "view", label: "مشاهده" },
|
|
{ key: "create", label: "ایجاد" },
|
|
{ key: "update", label: "ویرایش" },
|
|
{ key: "delete", label: "حذف" },
|
|
],
|
|
},
|
|
};
|
|
|
|
const ALL_ACTIONS = [
|
|
"view",
|
|
"create",
|
|
"update",
|
|
"delete",
|
|
"cancel",
|
|
"update_status",
|
|
];
|
|
const ACTION_HEADERS = [
|
|
"مشاهده",
|
|
"ایجاد",
|
|
"ویرایش",
|
|
"حذف",
|
|
"لغو",
|
|
"تغییر وضعیت",
|
|
];
|
|
|
|
function PermissionsMatrix({
|
|
permissions,
|
|
onChange,
|
|
}: {
|
|
permissions: SecretaryPermissions;
|
|
onChange: (p: SecretaryPermissions) => void;
|
|
}) {
|
|
const toggle = (section: PermSection, action: string) => {
|
|
const cur = (permissions[section] as Record<string, boolean>)[action];
|
|
onChange({
|
|
...permissions,
|
|
[section]: {
|
|
...(permissions[section] as Record<string, boolean>),
|
|
[action]: !cur,
|
|
},
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div style={{ overflowX: "auto" }}>
|
|
<table
|
|
style={{
|
|
width: "100%",
|
|
borderCollapse: "collapse",
|
|
fontSize: 13,
|
|
}}
|
|
>
|
|
<thead>
|
|
<tr style={{ background: "oklch(0.97 0.01 256)" }}>
|
|
<th
|
|
style={{
|
|
textAlign: "right",
|
|
padding: "10px 12px",
|
|
fontWeight: 600,
|
|
color: "var(--text-2)",
|
|
borderBottom: "2px solid var(--border)",
|
|
}}
|
|
>
|
|
بخش
|
|
</th>
|
|
{ACTION_HEADERS.map((h) => (
|
|
<th
|
|
key={h}
|
|
style={{
|
|
textAlign: "center",
|
|
padding: "10px 8px",
|
|
fontSize: 12,
|
|
fontWeight: 600,
|
|
color: "var(--text-2)",
|
|
borderBottom: "2px solid var(--border)",
|
|
}}
|
|
>
|
|
{h}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{(Object.keys(PERMISSION_LABELS) as PermSection[]).map(
|
|
(section, idx) => {
|
|
const config = PERMISSION_LABELS[section];
|
|
const sectionPrm = permissions[section] as Record<
|
|
string,
|
|
boolean
|
|
>;
|
|
return (
|
|
<tr
|
|
key={section}
|
|
style={{
|
|
background:
|
|
idx % 2 === 0
|
|
? "transparent"
|
|
: "oklch(0.985 0.005 256)",
|
|
}}
|
|
>
|
|
<td
|
|
style={{
|
|
padding: "10px 12px",
|
|
borderBottom:
|
|
"1px solid var(--border)",
|
|
fontWeight: 600,
|
|
}}
|
|
>
|
|
{config.label}
|
|
</td>
|
|
{ALL_ACTIONS.map((action) => {
|
|
const ac = config.actions.find(
|
|
(a) => a.key === action,
|
|
);
|
|
if (!ac)
|
|
return (
|
|
<td
|
|
key={action}
|
|
style={{
|
|
textAlign: "center",
|
|
borderBottom:
|
|
"1px solid var(--border)",
|
|
color: "var(--border)",
|
|
}}
|
|
>
|
|
—
|
|
</td>
|
|
);
|
|
return (
|
|
<td
|
|
key={action}
|
|
style={{
|
|
textAlign: "center",
|
|
borderBottom:
|
|
"1px solid var(--border)",
|
|
}}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={
|
|
sectionPrm[action] ??
|
|
false
|
|
}
|
|
onChange={() =>
|
|
toggle(section, action)
|
|
}
|
|
style={{
|
|
width: 16,
|
|
height: 16,
|
|
accentColor:
|
|
"var(--primary)",
|
|
cursor: "pointer",
|
|
}}
|
|
/>
|
|
</td>
|
|
);
|
|
})}
|
|
</tr>
|
|
);
|
|
},
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Create form schema ─────────────────────────────────────────────────────
|
|
|
|
const createSchema = z.object({
|
|
name: z.string().min(2, "نام الزامی است"),
|
|
mobile_number: z.string().regex(/^09[0-9]{9}$/, "شماره موبایل معتبر نیست"),
|
|
});
|
|
type CreateForm = z.infer<typeof createSchema>;
|
|
|
|
interface ClinicDoctor {
|
|
uuid: string;
|
|
name: string;
|
|
}
|
|
|
|
// ── Main component ─────────────────────────────────────────────────────────
|
|
|
|
export default function MySecretariesPage() {
|
|
const qc = useQueryClient();
|
|
const { doctorUuid, dbUuid, primaryRole } = useAuthStore();
|
|
|
|
const isClinic = primaryRole === "clinic";
|
|
const { maxSecretaries } = useSubscription();
|
|
|
|
// for clinic: selected doctor to add secretary for
|
|
const [selectedDoctorUuid, setSelectedDoctorUuid] = useState<string>("");
|
|
|
|
const activeDoctorUuid = isClinic ? selectedDoctorUuid : (doctorUuid ?? "");
|
|
|
|
const [createOpen, setCreateOpen] = useState(false);
|
|
const [editTarget, setEditTarget] = useState<Secretary | null>(null);
|
|
const [editPerms, setEditPerms] =
|
|
useState<SecretaryPermissions>(DEFAULT_PERMISSIONS);
|
|
const [deleteTarget, setDeleteTarget] = useState<Secretary | null>(null);
|
|
|
|
// clinic: load clinic's doctors
|
|
const { data: clinicDoctorsData, isLoading: clinicDoctorsLoading } =
|
|
useQuery<ApiResponse<{ data: ClinicDoctor[] }>>({
|
|
queryKey: ["clinic-doctors", dbUuid],
|
|
queryFn: () => api.get(`/api/v1/clinic/doctor-list/${dbUuid}`),
|
|
enabled: isClinic && !!dbUuid,
|
|
});
|
|
const clinicDoctors: ClinicDoctor[] = clinicDoctorsData?.data?.data ?? [];
|
|
|
|
// clinic: load ALL secretaries across all its doctors
|
|
const { data: clinicSecrData, isLoading: clinicSecrLoading } = useQuery<
|
|
ApiResponse<Secretary[]>
|
|
>({
|
|
queryKey: ["my-secretaries-clinic", dbUuid],
|
|
queryFn: () => api.get(`/api/v1/secretaries/clinic/${dbUuid}`),
|
|
enabled: isClinic && !!dbUuid,
|
|
});
|
|
|
|
// doctor: load secretaries for the doctor
|
|
const { data: doctorSecrData, isLoading: doctorSecrLoading } = useQuery<
|
|
ApiResponse<Secretary[]>
|
|
>({
|
|
queryKey: ["my-secretaries", activeDoctorUuid],
|
|
queryFn: () => api.get(`/api/v1/secretaries/${activeDoctorUuid}`),
|
|
enabled: !isClinic && !!activeDoctorUuid,
|
|
});
|
|
|
|
const secretaries = isClinic
|
|
? (clinicSecrData?.data ?? [])
|
|
: (doctorSecrData?.data ?? []);
|
|
const activeSecretaryCount = secretaries.filter((s) => s.is_active).length;
|
|
const isLoading = isClinic ? clinicSecrLoading : doctorSecrLoading;
|
|
|
|
const createForm = useForm<CreateForm>({
|
|
resolver: zodResolver(createSchema),
|
|
});
|
|
|
|
const createMutation = useMutation({
|
|
mutationFn: (body: CreateForm) =>
|
|
api.post("/api/v1/secretary", {
|
|
...body,
|
|
doctor_uuid: activeDoctorUuid,
|
|
}),
|
|
onSuccess: () => {
|
|
toast.success("منشی اضافه شد");
|
|
setCreateOpen(false);
|
|
createForm.reset();
|
|
qc.invalidateQueries({ queryKey: ["my-secretaries"] });
|
|
qc.invalidateQueries({ queryKey: ["my-secretaries-clinic"] });
|
|
},
|
|
onError: (e: any) => toast.error(e.message),
|
|
});
|
|
|
|
const updatePermsMutation = useMutation({
|
|
mutationFn: ({
|
|
uuid,
|
|
permissions,
|
|
}: {
|
|
uuid: string;
|
|
permissions: SecretaryPermissions;
|
|
}) =>
|
|
api.patch(`/api/v1/secretary/${uuid}`, {
|
|
permissions: { version: 1, resources: permissions },
|
|
}),
|
|
onSuccess: () => {
|
|
toast.success("دسترسیها بروزرسانی شد");
|
|
setEditTarget(null);
|
|
qc.invalidateQueries({ queryKey: ["my-secretaries"] });
|
|
qc.invalidateQueries({ queryKey: ["my-secretaries-clinic"] });
|
|
},
|
|
onError: (e: any) => toast.error(e.message),
|
|
});
|
|
|
|
const toggleActiveMutation = useMutation({
|
|
mutationFn: ({ uuid, active }: { uuid: string; active: boolean }) =>
|
|
api.patch(`/api/v1/secretary/${uuid}`, { active }),
|
|
onSuccess: (_, { active }) => {
|
|
toast.success(active ? "منشی فعال شد" : "منشی غیرفعال شد");
|
|
setDeleteTarget(null);
|
|
qc.invalidateQueries({ queryKey: ["my-secretaries"] });
|
|
qc.invalidateQueries({ queryKey: ["my-secretaries-clinic"] });
|
|
},
|
|
onError: (e: any) => toast.error(e.message),
|
|
});
|
|
|
|
const openEdit = (s: Secretary) => {
|
|
setEditTarget(s);
|
|
setEditPerms(s.permissions ?? DEFAULT_PERMISSIONS);
|
|
};
|
|
|
|
const handleCreateOpen = () => {
|
|
if (isClinic && !selectedDoctorUuid) {
|
|
toast.error("ابتدا یک پزشک را انتخاب کنید");
|
|
return;
|
|
}
|
|
setCreateOpen(true);
|
|
};
|
|
|
|
const selectedDoctorName =
|
|
clinicDoctors.find((d) => d.uuid === selectedDoctorUuid)?.name ?? "";
|
|
|
|
// for clinic: show doctor column in the table
|
|
const clinicColumns: Column<Secretary>[] = isClinic
|
|
? [
|
|
{
|
|
key: "doctor_name",
|
|
header: "پزشک",
|
|
render: (s) => (
|
|
<span
|
|
style={{
|
|
fontSize: 13,
|
|
color: "var(--text-2)",
|
|
fontWeight: 500,
|
|
}}
|
|
>
|
|
{s.doctor_name}
|
|
</span>
|
|
),
|
|
},
|
|
]
|
|
: [];
|
|
|
|
const allColumns: Column<Secretary>[] = [
|
|
{
|
|
key: "user_name",
|
|
header: "منشی",
|
|
render: (s) => (
|
|
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
<div
|
|
className="avatar sm"
|
|
style={{
|
|
background:
|
|
"linear-gradient(145deg, oklch(0.62 0.15 295), oklch(0.48 0.16 295))",
|
|
flexShrink: 0,
|
|
fontWeight: 700,
|
|
}}
|
|
>
|
|
{(s.user_name ?? "?").charAt(0)}
|
|
</div>
|
|
<div style={{ fontWeight: 600, fontSize: 14 }}>
|
|
{s.user_name}
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
...clinicColumns,
|
|
{
|
|
key: "mobile_number",
|
|
header: "موبایل",
|
|
render: (s) => (
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 5,
|
|
fontSize: 13,
|
|
color: "var(--text-2)",
|
|
}}
|
|
>
|
|
<PhoneIcon style={{ width: 13 }} />
|
|
<span dir="ltr">{maskMobile(s.mobile_number)}</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: "is_active",
|
|
header: "وضعیت",
|
|
render: (s) => <ActiveBadge active={s.is_active} />,
|
|
},
|
|
{
|
|
key: "created_at",
|
|
header: "تاریخ ثبت",
|
|
render: (s) => (
|
|
<span style={{ fontSize: 13, color: "var(--text-3)" }}>
|
|
{formatDate(Number(s.created_at))}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: "uuid",
|
|
header: "عملیات",
|
|
render: (s) => (
|
|
<div style={{ display: "flex", gap: 6 }}>
|
|
<button
|
|
className="btn sm"
|
|
onClick={() => openEdit(s)}
|
|
title="ویرایش دسترسیها"
|
|
>
|
|
<PencilIcon style={{ width: 14 }} />
|
|
</button>
|
|
<button
|
|
className="btn sm"
|
|
onClick={() => setDeleteTarget(s)}
|
|
title={s.is_active ? "غیرفعالسازی" : "فعالسازی"}
|
|
>
|
|
{s.is_active ? (
|
|
<NoSymbolIcon style={{ width: 14 }} />
|
|
) : (
|
|
<CheckCircleIcon
|
|
style={{ width: 14, color: "var(--success)" }}
|
|
/>
|
|
)}
|
|
</button>
|
|
</div>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<PageHeader
|
|
title="منشی ها من"
|
|
description="مدیریت منشی ها و دسترسیهای آنها"
|
|
action={
|
|
activeSecretaryCount >= maxSecretaries ? (
|
|
<Link
|
|
to="/admin/subscription"
|
|
className="btn sm"
|
|
style={{ textDecoration: "none", opacity: 0.8 }}
|
|
title={`حداکثر ${maxSecretaries} منشی مجاز است`}
|
|
>
|
|
ارتقاء پنل برای افزودن منشی
|
|
</Link>
|
|
) : (
|
|
<button
|
|
className="btn primary sm"
|
|
onClick={handleCreateOpen}
|
|
disabled={isClinic && !selectedDoctorUuid}
|
|
>
|
|
<PlusIcon style={{ width: 16 }} />
|
|
افزودن منشی
|
|
</button>
|
|
)
|
|
}
|
|
/>
|
|
|
|
{/* کلینیک: انتخاب پزشک برای افزودن منشی */}
|
|
{isClinic && (
|
|
<div className="card card-pad" style={{ marginBottom: 16 }}>
|
|
<div className="field" style={{ marginBottom: 0 }}>
|
|
<label>پزشک مورد نظر برای افزودن منشی جدید</label>
|
|
{clinicDoctorsLoading ? (
|
|
<p style={{ fontSize: 13, color: "var(--text-3)" }}>
|
|
در حال بارگذاری...
|
|
</p>
|
|
) : clinicDoctors.length === 0 ? (
|
|
<p style={{ fontSize: 13, color: "var(--text-3)" }}>
|
|
هیچ پزشکی در این کلینیک تعریف نشده است. ابتدا
|
|
پزشک اضافه کنید.
|
|
</p>
|
|
) : (
|
|
<select
|
|
value={selectedDoctorUuid}
|
|
onChange={(e) =>
|
|
setSelectedDoctorUuid(e.target.value)
|
|
}
|
|
style={{ width: "100%", maxWidth: 360 }}
|
|
>
|
|
<option value="">
|
|
— یک پزشک را انتخاب کنید —
|
|
</option>
|
|
{clinicDoctors.map((d) => (
|
|
<option key={d.uuid} value={d.uuid}>
|
|
{d.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{!isClinic && !doctorUuid ? (
|
|
<div
|
|
className="card card-pad"
|
|
style={{ textAlign: "center", color: "var(--text-3)" }}
|
|
>
|
|
پروفایل پزشک یافت نشد
|
|
</div>
|
|
) : (
|
|
<div className="card">
|
|
{secretaries.length === 0 && !isLoading ? (
|
|
<div
|
|
style={{
|
|
textAlign: "center",
|
|
padding: "60px 24px",
|
|
}}
|
|
>
|
|
<IdentificationIcon
|
|
style={{
|
|
width: 48,
|
|
color: "var(--text-3)",
|
|
margin: "0 auto 16px",
|
|
display: "block",
|
|
opacity: 0.4,
|
|
}}
|
|
/>
|
|
<div
|
|
style={{
|
|
fontWeight: 600,
|
|
fontSize: 15,
|
|
marginBottom: 8,
|
|
color: "var(--text-2)",
|
|
}}
|
|
>
|
|
هنوز منشیای اضافه نشده
|
|
</div>
|
|
<div
|
|
style={{
|
|
color: "var(--text-3)",
|
|
fontSize: 13.5,
|
|
marginBottom: 20,
|
|
}}
|
|
>
|
|
{isClinic
|
|
? "برای افزودن منشی، ابتدا یک پزشک را از لیست بالا انتخاب کنید"
|
|
: "منشی میتواند نوبتها و اطلاعات کلینیک را مدیریت کند"}
|
|
</div>
|
|
{!isClinic && (
|
|
<button
|
|
className="btn primary sm"
|
|
onClick={handleCreateOpen}
|
|
>
|
|
<PlusIcon style={{ width: 16 }} /> افزودن
|
|
اولین منشی
|
|
</button>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<DataTable
|
|
columns={allColumns}
|
|
data={secretaries}
|
|
loading={isLoading}
|
|
emptyMessage="منشیای ثبت نشده است"
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Modal افزودن منشی */}
|
|
<Modal
|
|
open={createOpen}
|
|
onClose={() => {
|
|
setCreateOpen(false);
|
|
createForm.reset();
|
|
}}
|
|
title="افزودن منشی جدید"
|
|
size="sm"
|
|
footer={
|
|
<>
|
|
<button
|
|
type="button"
|
|
className="btn ghost sm"
|
|
onClick={() => {
|
|
setCreateOpen(false);
|
|
createForm.reset();
|
|
}}
|
|
>
|
|
انصراف
|
|
</button>
|
|
<button
|
|
form="create-secretary-form"
|
|
type="submit"
|
|
className="btn primary sm"
|
|
disabled={createMutation.isPending}
|
|
>
|
|
{createMutation.isPending
|
|
? "در حال افزودن..."
|
|
: "افزودن منشی"}
|
|
</button>
|
|
</>
|
|
}
|
|
>
|
|
{isClinic && selectedDoctorName && (
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 8,
|
|
padding: "9px 12px",
|
|
marginBottom: 20,
|
|
background: "var(--primary-subtle)",
|
|
borderRadius: 8,
|
|
fontSize: 13,
|
|
color: "var(--primary)",
|
|
}}
|
|
>
|
|
<IdentificationIcon
|
|
style={{ width: 16, flexShrink: 0 }}
|
|
/>
|
|
منشی برای <strong>{selectedDoctorName}</strong> ثبت
|
|
میشود
|
|
</div>
|
|
)}
|
|
<form
|
|
id="create-secretary-form"
|
|
onSubmit={createForm.handleSubmit((d) =>
|
|
createMutation.mutate(d),
|
|
)}
|
|
>
|
|
<div className="field">
|
|
<label>نام و نام خانوادگی *</label>
|
|
<input
|
|
{...createForm.register("name")}
|
|
placeholder="مثال: سارا احمدی"
|
|
autoComplete="off"
|
|
autoFocus
|
|
style={
|
|
createForm.formState.errors.name
|
|
? { borderColor: "var(--danger)" }
|
|
: undefined
|
|
}
|
|
/>
|
|
{createForm.formState.errors.name && (
|
|
<span className="field-error">
|
|
{createForm.formState.errors.name.message}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="field" style={{ marginTop: 16 }}>
|
|
<label>شماره موبایل *</label>
|
|
<input
|
|
{...createForm.register("mobile_number")}
|
|
placeholder="09123456789"
|
|
dir="ltr"
|
|
inputMode="numeric"
|
|
maxLength={11}
|
|
autoComplete="off"
|
|
onChange={(e) => {
|
|
const digits = e.target.value.replace(
|
|
/\D/g,
|
|
"",
|
|
);
|
|
createForm.setValue("mobile_number", digits, {
|
|
shouldValidate:
|
|
createForm.formState.isSubmitted,
|
|
});
|
|
}}
|
|
style={
|
|
createForm.formState.errors.mobile_number
|
|
? { borderColor: "var(--danger)" }
|
|
: undefined
|
|
}
|
|
/>
|
|
{createForm.formState.errors.mobile_number && (
|
|
<span className="field-error">
|
|
{
|
|
createForm.formState.errors.mobile_number
|
|
.message
|
|
}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p
|
|
style={{
|
|
fontSize: 12.5,
|
|
color: "var(--text-3)",
|
|
margin: "4px 0 0",
|
|
lineHeight: 1.7,
|
|
}}
|
|
>
|
|
شماره باید با ۰۹ شروع شده و ۱۱ رقم باشد. اگر کاربری با
|
|
این شماره در سیستم وجود داشته باشد، به عنوان منشی اضافه
|
|
میشود.
|
|
</p>
|
|
</form>
|
|
</Modal>
|
|
|
|
{/* Modal ویرایش دسترسیها */}
|
|
<Modal
|
|
open={!!editTarget}
|
|
onClose={() => setEditTarget(null)}
|
|
title={`دسترسیهای ${editTarget?.user_name ?? ""}`}
|
|
size="lg"
|
|
footer={
|
|
<>
|
|
<button
|
|
className="btn ghost sm"
|
|
onClick={() => setEditTarget(null)}
|
|
>
|
|
لغو
|
|
</button>
|
|
<button
|
|
className="btn primary sm"
|
|
disabled={updatePermsMutation.isPending}
|
|
onClick={() =>
|
|
editTarget &&
|
|
updatePermsMutation.mutate({
|
|
uuid: editTarget.uuid,
|
|
permissions: editPerms,
|
|
})
|
|
}
|
|
>
|
|
{updatePermsMutation.isPending
|
|
? "در حال ذخیره..."
|
|
: "ذخیره دسترسیها"}
|
|
</button>
|
|
</>
|
|
}
|
|
>
|
|
<PermissionsMatrix
|
|
permissions={editPerms}
|
|
onChange={setEditPerms}
|
|
/>
|
|
</Modal>
|
|
|
|
{/* Confirm تغییر وضعیت منشی */}
|
|
<ConfirmDialog
|
|
open={!!deleteTarget}
|
|
title={
|
|
deleteTarget?.is_active
|
|
? "غیرفعالسازی منشی"
|
|
: "فعالسازی منشی"
|
|
}
|
|
message={
|
|
deleteTarget?.is_active
|
|
? `آیا مطمئن هستید که میخواهید منشی «${deleteTarget?.user_name}» را غیرفعال کنید؟`
|
|
: `آیا مطمئن هستید که میخواهید منشی «${deleteTarget?.user_name}» را دوباره فعال کنید؟`
|
|
}
|
|
confirmLabel={
|
|
deleteTarget?.is_active ? "غیرفعالسازی" : "فعالسازی"
|
|
}
|
|
danger={deleteTarget?.is_active}
|
|
loading={toggleActiveMutation.isPending}
|
|
onConfirm={() =>
|
|
deleteTarget &&
|
|
toggleActiveMutation.mutate({
|
|
uuid: deleteTarget.uuid,
|
|
active: !deleteTarget.is_active,
|
|
})
|
|
}
|
|
onCancel={() => setDeleteTarget(null)}
|
|
/>
|
|
</>
|
|
);
|
|
}
|