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>
|
<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 (
|
return (
|
||||||
<div className="space-y-2">
|
<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>
|
<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 (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
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 { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -132,7 +132,8 @@ function PermissionsMatrix({
|
|||||||
// ── Create form schema ─────────────────────────────────────────────────────
|
// ── Create form schema ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
const createSchema = z.object({
|
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>;
|
type CreateForm = z.infer<typeof createSchema>;
|
||||||
|
|
||||||
@@ -182,6 +183,7 @@ export default function MySecretariesPage() {
|
|||||||
const secretaries = isClinic
|
const secretaries = isClinic
|
||||||
? (clinicSecrData?.data ?? [])
|
? (clinicSecrData?.data ?? [])
|
||||||
: (doctorSecrData?.data ?? []);
|
: (doctorSecrData?.data ?? []);
|
||||||
|
const activeSecretaryCount = secretaries.filter((s) => s.is_active).length;
|
||||||
const isLoading = isClinic ? clinicSecrLoading : doctorSecrLoading;
|
const isLoading = isClinic ? clinicSecrLoading : doctorSecrLoading;
|
||||||
|
|
||||||
const createForm = useForm<CreateForm>({ resolver: zodResolver(createSchema) });
|
const createForm = useForm<CreateForm>({ resolver: zodResolver(createSchema) });
|
||||||
@@ -211,10 +213,11 @@ export default function MySecretariesPage() {
|
|||||||
onError: (e: any) => toast.error(e.message),
|
onError: (e: any) => toast.error(e.message),
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
const toggleActiveMutation = useMutation({
|
||||||
mutationFn: (uuid: string) => api.delete(`/api/v1/secretary/${uuid}`),
|
mutationFn: ({ uuid, active }: { uuid: string; active: boolean }) =>
|
||||||
onSuccess: () => {
|
api.patch(`/api/v1/secretary/${uuid}`, { active }),
|
||||||
toast.success('منشی غیرفعال شد');
|
onSuccess: (_, { active }) => {
|
||||||
|
toast.success(active ? 'منشی فعال شد' : 'منشی غیرفعال شد');
|
||||||
setDeleteTarget(null);
|
setDeleteTarget(null);
|
||||||
qc.invalidateQueries({ queryKey: ['my-secretaries'] });
|
qc.invalidateQueries({ queryKey: ['my-secretaries'] });
|
||||||
qc.invalidateQueries({ queryKey: ['my-secretaries-clinic'] });
|
qc.invalidateQueries({ queryKey: ['my-secretaries-clinic'] });
|
||||||
@@ -298,8 +301,15 @@ export default function MySecretariesPage() {
|
|||||||
<button className="btn sm" onClick={() => openEdit(s)} title="ویرایش دسترسیها">
|
<button className="btn sm" onClick={() => openEdit(s)} title="ویرایش دسترسیها">
|
||||||
<PencilIcon style={{ width: 14 }} />
|
<PencilIcon style={{ width: 14 }} />
|
||||||
</button>
|
</button>
|
||||||
<button className="btn sm" onClick={() => setDeleteTarget(s)} title="غیرفعالسازی">
|
<button
|
||||||
<TrashIcon style={{ width: 14 }} />
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -312,7 +322,7 @@ export default function MySecretariesPage() {
|
|||||||
title="منشیان من"
|
title="منشیان من"
|
||||||
description="مدیریت منشیان و دسترسیهای آنها"
|
description="مدیریت منشیان و دسترسیهای آنها"
|
||||||
action={
|
action={
|
||||||
secretaries.length >= maxSecretaries ? (
|
activeSecretaryCount >= maxSecretaries ? (
|
||||||
<Link to="/admin/subscription" className="btn sm" style={{ textDecoration: 'none', opacity: 0.8 }} title={`حداکثر ${maxSecretaries} منشی مجاز است`}>
|
<Link to="/admin/subscription" className="btn sm" style={{ textDecoration: 'none', opacity: 0.8 }} title={`حداکثر ${maxSecretaries} منشی مجاز است`}>
|
||||||
ارتقاء پنل برای افزودن منشی
|
ارتقاء پنل برای افزودن منشی
|
||||||
</Link>
|
</Link>
|
||||||
@@ -378,33 +388,74 @@ export default function MySecretariesPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Modal افزودن منشی */}
|
{/* 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 && (
|
{isClinic && selectedDoctorName && (
|
||||||
<div style={{ marginBottom: 12, padding: '8px 12px', background: 'var(--primary-subtle)', borderRadius: 8, fontSize: 13, color: 'var(--primary)' }}>
|
<div style={{
|
||||||
منشی برای دکتر {selectedDoctorName} اضافه میشود
|
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
<form onSubmit={createForm.handleSubmit((d) => createMutation.mutate(d))}>
|
<form id="create-secretary-form" onSubmit={createForm.handleSubmit((d) => createMutation.mutate(d))}>
|
||||||
<div className="field">
|
<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>
|
<label>شماره موبایل *</label>
|
||||||
<input
|
<input
|
||||||
{...createForm.register('mobile_number')}
|
{...createForm.register('mobile_number')}
|
||||||
placeholder="09123456789"
|
placeholder="09123456789"
|
||||||
dir="ltr"
|
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 && (
|
{createForm.formState.errors.mobile_number && (
|
||||||
<span className="field-error">{createForm.formState.errors.mobile_number.message}</span>
|
<span className="field-error">{createForm.formState.errors.mobile_number.message}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</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>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
@@ -430,15 +481,18 @@ export default function MySecretariesPage() {
|
|||||||
<PermissionsMatrix permissions={editPerms} onChange={setEditPerms} />
|
<PermissionsMatrix permissions={editPerms} onChange={setEditPerms} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* Confirm غیرفعالسازی */}
|
{/* Confirm تغییر وضعیت منشی */}
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={!!deleteTarget}
|
open={!!deleteTarget}
|
||||||
title="غیرفعالسازی منشی"
|
title={deleteTarget?.is_active ? 'غیرفعالسازی منشی' : 'فعالسازی منشی'}
|
||||||
message={`آیا مطمئن هستید که میخواهید منشی «${deleteTarget?.user_name}» را غیرفعال کنید؟`}
|
message={deleteTarget?.is_active
|
||||||
confirmLabel="غیرفعالسازی"
|
? `آیا مطمئن هستید که میخواهید منشی «${deleteTarget?.user_name}» را غیرفعال کنید؟`
|
||||||
danger
|
: `آیا مطمئن هستید که میخواهید منشی «${deleteTarget?.user_name}» را دوباره فعال کنید؟`
|
||||||
loading={deleteMutation.isPending}
|
}
|
||||||
onConfirm={() => deleteTarget && deleteMutation.mutate(deleteTarget.uuid)}
|
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)}
|
onCancel={() => setDeleteTarget(null)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ class SecretaryController extends BaseController
|
|||||||
$secretaryUser->setPasswordHash($hash);
|
$secretaryUser->setPasswordHash($hash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!empty($data['name'])) {
|
||||||
|
$secretaryUser->setRealName(trim($data['name']));
|
||||||
|
}
|
||||||
|
|
||||||
// Assign ROLE_SECRETARY
|
// Assign ROLE_SECRETARY
|
||||||
$roles = $secretaryUser->getRoles();
|
$roles = $secretaryUser->getRoles();
|
||||||
@@ -151,7 +154,7 @@ class SecretaryController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/api/v1/secretary/{uuid}', methods: ['DELETE'])]
|
#[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);
|
$secretary = $this->secretaryRepo->findByUuid($uuid);
|
||||||
if ($secretary === null) {
|
if ($secretary === null) {
|
||||||
@@ -162,9 +165,10 @@ class SecretaryController extends BaseController
|
|||||||
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
|
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