feat(secretary): enhance secretary management with name input and toggle activation feature
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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' => 'منشی غیرفعال شد']);
|
||||
}
|
||||
|
||||
/** لیست منشیان مطب شخصی یک دکتر */
|
||||
|
||||
Reference in New Issue
Block a user