feat: enhance StaffPage modals and form fields with improved layout and error handling
This commit is contained in:
@@ -235,32 +235,47 @@ export default function StaffPage() {
|
||||
</div>
|
||||
|
||||
{/* Modal ایجاد */}
|
||||
<Modal open={createOpen} onClose={() => setCreateOpen(false)} title="افزودن پرسنل جدید">
|
||||
<form onSubmit={createForm.handleSubmit((d) => createMutation.mutate(d))}>
|
||||
<StaffFormFields form={createForm} />
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
|
||||
<button type="submit" className="btn primary" disabled={createMutation.isPending}>
|
||||
{createMutation.isPending ? 'در حال ذخیره...' : 'ذخیره'}
|
||||
<Modal
|
||||
open={createOpen}
|
||||
onClose={() => setCreateOpen(false)}
|
||||
title="افزودن پرسنل جدید"
|
||||
size="lg"
|
||||
footer={
|
||||
<>
|
||||
<button type="button" className="btn ghost sm" onClick={() => setCreateOpen(false)}>انصراف</button>
|
||||
<button form="staff-create-form" type="submit" className="btn primary sm" disabled={createMutation.isPending}>
|
||||
{createMutation.isPending ? 'در حال ذخیره...' : 'ذخیره پرسنل'}
|
||||
</button>
|
||||
<button type="button" className="btn" onClick={() => setCreateOpen(false)}>انصراف</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<form id="staff-create-form" onSubmit={createForm.handleSubmit((d) => createMutation.mutate(d))}>
|
||||
<StaffFormFields form={createForm} mode="create" />
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
{/* Modal ویرایش */}
|
||||
<Modal open={!!editTarget} onClose={() => setEditTarget(null)} title="ویرایش پرسنل">
|
||||
<Modal
|
||||
open={!!editTarget}
|
||||
onClose={() => setEditTarget(null)}
|
||||
title={editTarget ? `ویرایش — ${editTarget.full_name}` : 'ویرایش پرسنل'}
|
||||
size="lg"
|
||||
footer={
|
||||
<>
|
||||
<button type="button" className="btn ghost sm" onClick={() => setEditTarget(null)}>انصراف</button>
|
||||
<button form="staff-edit-form" type="submit" className="btn primary sm" disabled={editMutation.isPending}>
|
||||
{editMutation.isPending ? 'در حال ذخیره...' : 'ذخیره تغییرات'}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<form
|
||||
id="staff-edit-form"
|
||||
onSubmit={editForm.handleSubmit((d) =>
|
||||
editTarget && editMutation.mutate({ uuid: editTarget.uuid, body: d })
|
||||
)}
|
||||
>
|
||||
<StaffFormFields form={editForm} />
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
|
||||
<button type="submit" className="btn primary" disabled={editMutation.isPending}>
|
||||
{editMutation.isPending ? 'در حال ذخیره...' : 'ذخیره'}
|
||||
</button>
|
||||
<button type="button" className="btn" onClick={() => setEditTarget(null)}>انصراف</button>
|
||||
</div>
|
||||
<StaffFormFields form={editForm} mode="edit" />
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
@@ -278,46 +293,94 @@ export default function StaffPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function StaffFormFields({ form }: { form: ReturnType<typeof useForm<StaffFormData>> }) {
|
||||
const { register, formState: { errors } } = form;
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
||||
<div className="field">
|
||||
<label>نام و نام خانوادگی *</label>
|
||||
<input {...register('full_name')} placeholder="علی رضایی" />
|
||||
{errors.full_name && <span className="field-error">{errors.full_name.message}</span>}
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>سمت</label>
|
||||
<input {...register('job_title')} placeholder="پرستار" />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
||||
<div className="field">
|
||||
<label>موبایل (نام کاربری ورود) *</label>
|
||||
<input {...numericField(register('phone'), 11)} placeholder="09121234567" />
|
||||
{errors.phone && <span className="field-error">{errors.phone.message}</span>}
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>کد ملی</label>
|
||||
<input {...numericField(register('national_code'), 10)} placeholder="0012345678" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>آدرس</label>
|
||||
<input {...register('address')} placeholder="آدرس محل سکونت" />
|
||||
</div>
|
||||
const Required = () => <span style={{ color: 'var(--danger)' }}>*</span>;
|
||||
|
||||
<div className="field" style={{ borderTop: '1px solid var(--border)', paddingTop: 12 }}>
|
||||
<label>رمز عبور ورود به پنل</label>
|
||||
<input type="password" autoComplete="new-password" {...register('password')} placeholder="حداقل ۸ کاراکتر — خالی یعنی بدون تغییر" />
|
||||
{errors.password && <span className="field-error">{errors.password.message}</span>}
|
||||
<div style={{ fontSize: 12, color: 'var(--text-3)', marginTop: 4 }}>
|
||||
برای هر پرسنل حساب کاربری ساخته میشود: با همین شماره موبایل وارد پنل میشود و فقط
|
||||
داشبورد و سرویسهای خودش را میبیند.
|
||||
</div>
|
||||
/** تیتر یک بخش از فرم — همان الگوی سایر فرمهای چندبخشی پنل. */
|
||||
function FormSection({ title, hint, children }: { title: string; hint?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<section style={{ marginBottom: 22 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 12 }}>
|
||||
<h3 style={{ fontSize: 14, fontWeight: 700, color: 'var(--text)' }}>{title}</h3>
|
||||
{hint && <span style={{ fontSize: 12, color: 'var(--text-3)' }}>{hint}</span>}
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function StaffFormFields({ form, mode }: { form: ReturnType<typeof useForm<StaffFormData>>; mode: 'create' | 'edit' }) {
|
||||
const { register, formState: { errors } } = form;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormSection title="مشخصات پرسنل">
|
||||
<div className="grid-2" style={{ gap: 14, marginBottom: 0 }}>
|
||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||
<label>نام و نام خانوادگی <Required /></label>
|
||||
<input {...register('full_name')} className={`input ${errors.full_name ? 'err' : ''}`} placeholder="علی رضایی" />
|
||||
{errors.full_name && <p className="err-text">{errors.full_name.message}</p>}
|
||||
</div>
|
||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||
<label>سمت</label>
|
||||
<input {...register('job_title')} className="input" placeholder="پرستار" />
|
||||
</div>
|
||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||
<label>کد ملی</label>
|
||||
<input {...numericField(register('national_code'), 10)} className="input" dir="ltr" inputMode="numeric" placeholder="0012345678" />
|
||||
</div>
|
||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||
<label>آدرس</label>
|
||||
<input {...register('address')} className="input" placeholder="آدرس محل سکونت" />
|
||||
</div>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSection title="حساب کاربری ورود به پنل" hint="برای هر پرسنل ساخته میشود">
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--primary-soft)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r)',
|
||||
padding: 16,
|
||||
}}
|
||||
>
|
||||
<p style={{ fontSize: 12.5, color: 'var(--text-2)', lineHeight: 1.9, marginBottom: 14 }}>
|
||||
پرسنل با همین شمارهٔ موبایل وارد پنل میشود و فقط داشبورد و سرویسهای خودش را میبیند.
|
||||
</p>
|
||||
<div className="grid-2" style={{ gap: 14, marginBottom: 0 }}>
|
||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||
<label>موبایل — نام کاربری <Required /></label>
|
||||
<input
|
||||
{...numericField(register('phone'), 11)}
|
||||
className={`input ${errors.phone ? 'err' : ''}`}
|
||||
dir="ltr"
|
||||
inputMode="numeric"
|
||||
autoComplete="off"
|
||||
placeholder="09121234567"
|
||||
/>
|
||||
{errors.phone && <p className="err-text">{errors.phone.message}</p>}
|
||||
</div>
|
||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||
<label>رمز عبور</label>
|
||||
<input
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
{...register('password')}
|
||||
className={`input ${errors.password ? 'err' : ''}`}
|
||||
dir="ltr"
|
||||
placeholder={mode === 'edit' ? 'خالی = بدون تغییر' : 'حداقل ۸ کاراکتر'}
|
||||
/>
|
||||
{errors.password
|
||||
? <p className="err-text">{errors.password.message}</p>
|
||||
: <p style={{ fontSize: 11.5, color: 'var(--text-3)' }}>
|
||||
{mode === 'edit'
|
||||
? 'برای تغییر رمز، مقدار جدید را وارد کنید.'
|
||||
: 'خالی بگذارید تا پرسنل خودش با «فراموشی رمز» تعیین کند.'}
|
||||
</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormSection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -579,6 +579,9 @@ body {
|
||||
/* field-label utility */
|
||||
.field-label { display: block; font-size: 12.5px; font-weight: 600; color: var(--text-2); margin-bottom: 5px; }
|
||||
|
||||
/* پیام خطای اعتبارسنجی زیر ورودی — هر دو نام در صفحات موجود استفاده شدهاند */
|
||||
.err-text, .field-error { display: block; font-size: 11.5px; font-weight: 600; color: var(--danger); }
|
||||
|
||||
/* ── Buttons ─────────────────────────────────────────────────── */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
||||
|
||||
Reference in New Issue
Block a user