feat(secretary): enhance secretary management with name input and toggle activation feature

This commit is contained in:
hamed
2026-06-15 12:23:07 +03:30
parent fa6656df48
commit 95f5918118
3 changed files with 113 additions and 31 deletions
+24
View File
@@ -1274,6 +1274,18 @@ function WeeklyScheduleTab({ doctorUuid, addresses }: { doctorUuid: string; addr
<div className="space-y-3">{Array.from({ length: 4 }).map((_, i) => <div key={i} className="h-12 rounded-xl skeleton" />)}</div>
);
if (addresses.length === 0) return (
<div className="flex flex-col items-center justify-center gap-3 py-12 text-center">
<div className="w-12 h-12 rounded-full bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/30 flex items-center justify-center">
<MapPinIcon className="w-6 h-6 text-amber-500" />
</div>
<div>
<p className="text-sm font-medium text-slate-700 dark:text-slate-200">ابتدا آدرس مطب را ثبت کنید</p>
<p className="text-xs text-slate-400 dark:text-slate-500 mt-1">برنامه کاری نیاز به حداقل یک مکان نوبت دارد</p>
</div>
</div>
);
return (
<div className="space-y-2">
{/* ─ خلاصه کل هفته */}
@@ -1537,6 +1549,18 @@ function DateOverridesTab({ doctorUuid, addresses }: { doctorUuid: string; addre
<div className="space-y-2">{Array.from({ length: 3 }).map((_, i) => <div key={i} className="h-14 rounded-xl skeleton" />)}</div>
);
if (addresses.length === 0) return (
<div className="flex flex-col items-center justify-center gap-3 py-12 text-center">
<div className="w-12 h-12 rounded-full bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/30 flex items-center justify-center">
<MapPinIcon className="w-6 h-6 text-amber-500" />
</div>
<div>
<p className="text-sm font-medium text-slate-700 dark:text-slate-200">ابتدا آدرس مطب را ثبت کنید</p>
<p className="text-xs text-slate-400 dark:text-slate-500 mt-1">برنامه کاری نیاز به حداقل یک مکان نوبت دارد</p>
</div>
</div>
);
return (
<div className="space-y-3">
<div className="flex justify-end">
+82 -28
View File
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { PlusIcon, PencilIcon, TrashIcon, IdentificationIcon, PhoneIcon } from '@heroicons/react/24/outline';
import { PlusIcon, PencilIcon, NoSymbolIcon, CheckCircleIcon, IdentificationIcon, PhoneIcon } from '@heroicons/react/24/outline';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
@@ -132,7 +132,8 @@ function PermissionsMatrix({
// ── Create form schema ─────────────────────────────────────────────────────
const createSchema = z.object({
mobile_number: z.string().min(10, 'شماره موبایل معتبر نیست'),
name: z.string().min(2, 'نام الزامی است'),
mobile_number: z.string().regex(/^09[0-9]{9}$/, 'شماره موبایل معتبر نیست'),
});
type CreateForm = z.infer<typeof createSchema>;
@@ -182,6 +183,7 @@ export default function MySecretariesPage() {
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) });
@@ -211,10 +213,11 @@ export default function MySecretariesPage() {
onError: (e: any) => toast.error(e.message),
});
const deleteMutation = useMutation({
mutationFn: (uuid: string) => api.delete(`/api/v1/secretary/${uuid}`),
onSuccess: () => {
toast.success('منشی غیرفعال شد');
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'] });
@@ -298,8 +301,15 @@ export default function MySecretariesPage() {
<button className="btn sm" onClick={() => openEdit(s)} title="ویرایش دسترسی‌ها">
<PencilIcon style={{ width: 14 }} />
</button>
<button className="btn sm" onClick={() => setDeleteTarget(s)} title="غیرفعال‌سازی">
<TrashIcon style={{ width: 14 }} />
<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>
),
@@ -312,7 +322,7 @@ export default function MySecretariesPage() {
title="منشیان من"
description="مدیریت منشیان و دسترسی‌های آن‌ها"
action={
secretaries.length >= maxSecretaries ? (
activeSecretaryCount >= maxSecretaries ? (
<Link to="/admin/subscription" className="btn sm" style={{ textDecoration: 'none', opacity: 0.8 }} title={`حداکثر ${maxSecretaries} منشی مجاز است`}>
ارتقاء پنل برای افزودن منشی
</Link>
@@ -378,33 +388,74 @@ export default function MySecretariesPage() {
)}
{/* Modal افزودن منشی */}
<Modal open={createOpen} onClose={() => setCreateOpen(false)} title="افزودن منشی جدید">
<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={{ marginBottom: 12, padding: '8px 12px', background: 'var(--primary-subtle)', borderRadius: 8, fontSize: 13, color: 'var(--primary)' }}>
منشی برای دکتر {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 onSubmit={createForm.handleSubmit((d) => createMutation.mutate(d))}>
<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: '8px 0 16px' }}>
کاربری با این شماره در سیستم جستجو شده و به عنوان منشی اضافه میشود.
<p style={{ fontSize: 12.5, color: 'var(--text-3)', margin: '4px 0 0', lineHeight: 1.7 }}>
شماره باید با ۰۹ شروع شده و ۱۱ رقم باشد. اگر کاربری با این شماره در سیستم وجود داشته باشد، به عنوان منشی اضافه میشود.
</p>
<div style={{ display: 'flex', gap: 8 }}>
<button type="submit" className="btn primary" disabled={createMutation.isPending}>
{createMutation.isPending ? 'در حال افزودن...' : 'افزودن'}
</button>
<button type="button" className="btn" onClick={() => setCreateOpen(false)}>انصراف</button>
</div>
</form>
</Modal>
@@ -430,15 +481,18 @@ export default function MySecretariesPage() {
<PermissionsMatrix permissions={editPerms} onChange={setEditPerms} />
</Modal>
{/* Confirm غیرفعال‌سازی */}
{/* Confirm تغییر وضعیت منشی */}
<ConfirmDialog
open={!!deleteTarget}
title="غیرفعال‌سازی منشی"
message={`آیا مطمئن هستید که می‌خواهید منشی «${deleteTarget?.user_name}» را غیرفعال کنید؟`}
confirmLabel="غیرفعال‌سازی"
danger
loading={deleteMutation.isPending}
onConfirm={() => deleteTarget && deleteMutation.mutate(deleteTarget.uuid)}
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)}
/>
</>
@@ -78,6 +78,9 @@ class SecretaryController extends BaseController
$secretaryUser->setPasswordHash($hash);
}
}
if (!empty($data['name'])) {
$secretaryUser->setRealName(trim($data['name']));
}
// Assign ROLE_SECRETARY
$roles = $secretaryUser->getRoles();
@@ -151,7 +154,7 @@ class SecretaryController extends BaseController
}
#[Route('/api/v1/secretary/{uuid}', methods: ['DELETE'])]
public function delete(string $uuid, #[CurrentUser] User $currentUser): JsonResponse
public function deactivate(string $uuid, #[CurrentUser] User $currentUser): JsonResponse
{
$secretary = $this->secretaryRepo->findByUuid($uuid);
if ($secretary === null) {
@@ -162,9 +165,10 @@ class SecretaryController extends BaseController
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
}
$this->secretaryRepo->remove($secretary);
$secretary->setActive(false);
$this->secretaryRepo->save($secretary);
return $this->success(['message' => 'منشی با موفقیت حذف شد']);
return $this->success(['message' => 'منشی غیرفعال شد']);
}
/** لیست منشیان مطب شخصی یک دکتر */