- Added BackButton component to standardize back navigation across pages. - Integrated BackButton into various pages, replacing custom back buttons for consistency. - Updated PageHeader to accept backTo prop for displaying BackButton when navigating from subpages. - Created useGoBack hook to handle navigation logic, determining whether to go back in history or redirect to a fallback page. - Added tests for BackButton and its integration with PageHeader to ensure expected behavior.
86 lines
4.8 KiB
TypeScript
86 lines
4.8 KiB
TypeScript
import React from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useMutation } from '@tanstack/react-query';
|
|
import { useForm } from 'react-hook-form';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { z } from 'zod';
|
|
import { ArrowRightIcon, BuildingOffice2Icon } from '@heroicons/react/24/outline';
|
|
import { toast } from 'sonner';
|
|
import { api } from '../lib/api';
|
|
import type { ApiResponse } from '../lib/api';
|
|
import MobileInput from '../components/ui/MobileInput';
|
|
import { iranMobileSchema } from '../lib/utils';
|
|
import BackButton from '../components/ui/BackButton';
|
|
import { latinDigitsField } from '../lib/forms';
|
|
|
|
const schema = z.object({
|
|
owner_mobile: iranMobileSchema,
|
|
name: z.string().min(2, 'نام کلینیک حداقل ۲ کاراکتر'),
|
|
telephone: z.string().max(20).optional().or(z.literal('')),
|
|
address: z.string().max(500).optional().or(z.literal('')),
|
|
info: z.string().max(2000).optional().or(z.literal('')),
|
|
});
|
|
type FormValues = z.infer<typeof schema>;
|
|
|
|
interface ClinicCreated { uuid: string; name: string; is_active: boolean }
|
|
|
|
export default function ClinicFormPage() {
|
|
const navigate = useNavigate();
|
|
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<FormValues>({ resolver: zodResolver(schema) });
|
|
|
|
const mutation = useMutation<ApiResponse<ClinicCreated>, Error, FormValues>({
|
|
mutationFn: (body) => api.post('/api/v1/admin/clinic', body),
|
|
onSuccess: (res) => {
|
|
toast.success(`کلینیک "${res?.data?.name}" ایجاد شد`);
|
|
navigate('/admin/clinics');
|
|
},
|
|
onError: (err) => toast.error(err.message ?? 'خطا در ایجاد کلینیک'),
|
|
});
|
|
|
|
return (
|
|
<div className="page" style={{ maxWidth: 640 }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 28 }}>
|
|
<BackButton fallback="/admin/clinics" />
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<BuildingOffice2Icon style={{ width: 22, height: 22, color: 'var(--primary)' }} />
|
|
<h1 style={{ margin: 0, fontSize: 20, fontWeight: 700 }}>افزودن کلینیک جدید</h1>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card">
|
|
<form onSubmit={handleSubmit((v) => mutation.mutate(v))} style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
<label style={{ fontSize: 13, fontWeight: 600 }}>شماره موبایل صاحب کلینیک <span style={{ color: 'var(--red)' }}>*</span></label>
|
|
<MobileInput className="field" hasError={!!errors.owner_mobile} {...register('owner_mobile')} />
|
|
{errors.owner_mobile && <span style={{ color: 'var(--red)', fontSize: 12 }}>{errors.owner_mobile.message}</span>}
|
|
<span style={{ fontSize: 11, color: 'var(--text-3)' }}>اگر این موبایل در سیستم نباشد، کاربر جدید ساخته میشود</span>
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
<label style={{ fontSize: 13, fontWeight: 600 }}>نام کلینیک <span style={{ color: 'var(--red)' }}>*</span></label>
|
|
<input className="field" placeholder="مثال: کلینیک تخصصی پارسیان" {...register('name')} />
|
|
{errors.name && <span style={{ color: 'var(--red)', fontSize: 12 }}>{errors.name.message}</span>}
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
<label style={{ fontSize: 13, fontWeight: 600 }}>تلفن ثابت</label>
|
|
<input className="field" placeholder="02xxxxxxxx" {...latinDigitsField(register('telephone'))} />
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
<label style={{ fontSize: 13, fontWeight: 600 }}>آدرس</label>
|
|
<input className="field" placeholder="آدرس کلینیک" {...register('address')} />
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
<label style={{ fontSize: 13, fontWeight: 600 }}>توضیحات</label>
|
|
<textarea className="field" rows={3} placeholder="درباره کلینیک..." {...register('info')} style={{ resize: 'vertical' }} />
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 12, justifyContent: 'flex-end', paddingTop: 8 }}>
|
|
<button type="button" className="btn ghost" onClick={() => navigate('/admin/clinics')}>انصراف</button>
|
|
<button type="submit" className="btn primary" disabled={isSubmitting || mutation.isPending}>
|
|
{mutation.isPending ? 'در حال ذخیره...' : 'ایجاد کلینیک'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|