Files
clinicpro/assets/admin/pages/ClinicFormPage.tsx
T
hamed e0e8fbd1e4 feat: implement BackButton component for consistent navigation
- 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.
2026-07-29 20:26:51 +03:30

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>
);
}