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>
|
</div>
|
||||||
|
|
||||||
{/* Modal ایجاد */}
|
{/* Modal ایجاد */}
|
||||||
<Modal open={createOpen} onClose={() => setCreateOpen(false)} title="افزودن پرسنل جدید">
|
<Modal
|
||||||
<form onSubmit={createForm.handleSubmit((d) => createMutation.mutate(d))}>
|
open={createOpen}
|
||||||
<StaffFormFields form={createForm} />
|
onClose={() => setCreateOpen(false)}
|
||||||
<div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
|
title="افزودن پرسنل جدید"
|
||||||
<button type="submit" className="btn primary" disabled={createMutation.isPending}>
|
size="lg"
|
||||||
{createMutation.isPending ? 'در حال ذخیره...' : 'ذخیره'}
|
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>
|
||||||
<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>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* 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
|
<form
|
||||||
|
id="staff-edit-form"
|
||||||
onSubmit={editForm.handleSubmit((d) =>
|
onSubmit={editForm.handleSubmit((d) =>
|
||||||
editTarget && editMutation.mutate({ uuid: editTarget.uuid, body: d })
|
editTarget && editMutation.mutate({ uuid: editTarget.uuid, body: d })
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<StaffFormFields form={editForm} />
|
<StaffFormFields form={editForm} mode="edit" />
|
||||||
<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>
|
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
@@ -278,46 +293,94 @@ export default function StaffPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StaffFormFields({ form }: { form: ReturnType<typeof useForm<StaffFormData>> }) {
|
const Required = () => <span style={{ color: 'var(--danger)' }}>*</span>;
|
||||||
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>
|
|
||||||
|
|
||||||
<div className="field" style={{ borderTop: '1px solid var(--border)', paddingTop: 12 }}>
|
/** تیتر یک بخش از فرم — همان الگوی سایر فرمهای چندبخشی پنل. */
|
||||||
<label>رمز عبور ورود به پنل</label>
|
function FormSection({ title, hint, children }: { title: string; hint?: string; children: React.ReactNode }) {
|
||||||
<input type="password" autoComplete="new-password" {...register('password')} placeholder="حداقل ۸ کاراکتر — خالی یعنی بدون تغییر" />
|
return (
|
||||||
{errors.password && <span className="field-error">{errors.password.message}</span>}
|
<section style={{ marginBottom: 22 }}>
|
||||||
<div style={{ fontSize: 12, color: 'var(--text-3)', marginTop: 4 }}>
|
<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>
|
|
||||||
</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 utility */
|
||||||
.field-label { display: block; font-size: 12.5px; font-weight: 600; color: var(--text-2); margin-bottom: 5px; }
|
.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 ─────────────────────────────────────────────────── */
|
/* ── Buttons ─────────────────────────────────────────────────── */
|
||||||
.btn {
|
.btn {
|
||||||
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
||||||
|
|||||||
Reference in New Issue
Block a user