Files
clinicpro/assets/admin/pages/StaffPage.tsx
T

214 lines
8.0 KiB
TypeScript

import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { PencilIcon, EyeIcon, EyeSlashIcon, PlusIcon } from '@heroicons/react/24/outline';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { toast } from 'sonner';
import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
import type { ClinicStaff } from '../types';
import { formatDate } from '../lib/utils';
import DataTable, { type Column } from '../components/ui/DataTable';
import Modal from '../components/ui/Modal';
import ConfirmDialog from '../components/ui/ConfirmDialog';
import PageHeader from '../components/ui/PageHeader';
import { ActiveBadge } from '../components/ui/StatusBadge';
const schema = z.object({
full_name: z.string().min(2, 'نام حداقل ۲ کاراکتر باید باشد'),
phone: z.string().optional(),
job_title: z.string().optional(),
address: z.string().optional(),
national_code: z.string().optional(),
});
type StaffFormData = z.infer<typeof schema>;
const EMPTY: ClinicStaff[] = [];
export default function StaffPage() {
const qc = useQueryClient();
const [createOpen, setCreateOpen] = useState(false);
const [editTarget, setEditTarget] = useState<ClinicStaff | null>(null);
const [toggleTarget, setToggleTarget] = useState<ClinicStaff | null>(null);
const { data, isLoading } = useQuery<ApiResponse<ClinicStaff[]>>({
queryKey: ['staff'],
queryFn: () => api.get('/api/v1/staff'),
});
const staff = data?.data ?? EMPTY;
const createForm = useForm<StaffFormData>({ resolver: zodResolver(schema) });
const editForm = useForm<StaffFormData>({ resolver: zodResolver(schema) });
const createMutation = useMutation({
mutationFn: (body: StaffFormData) => api.post('/api/v1/staff', body),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['staff'] });
setCreateOpen(false);
createForm.reset();
toast.success('پرسنل ایجاد شد');
},
onError: (err: any) => toast.error(err.message),
});
const editMutation = useMutation({
mutationFn: ({ uuid, body }: { uuid: string; body: StaffFormData }) =>
api.patch(`/api/v1/staff/${uuid}`, body),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['staff'] });
setEditTarget(null);
toast.success('اطلاعات پرسنل ویرایش شد');
},
onError: (err: any) => toast.error(err.message),
});
const toggleMutation = useMutation({
mutationFn: (uuid: string) => api.patch(`/api/v1/staff/${uuid}/toggle`, {}),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['staff'] });
setToggleTarget(null);
toast.success('وضعیت پرسنل تغییر کرد');
},
onError: (err: any) => toast.error(err.message),
});
const openEdit = (s: ClinicStaff) => {
editForm.reset({
full_name: s.full_name,
phone: s.phone ?? '',
job_title: s.job_title ?? '',
address: s.address ?? '',
national_code: s.national_code ?? '',
});
setEditTarget(s);
};
const columns: Column<ClinicStaff>[] = [
{ key: 'full_name', header: 'نام و نام خانوادگی' },
{ key: 'job_title', header: 'سمت', render: (s) => s.job_title ?? '—' },
{ key: 'phone', header: 'تلفن', render: (s) => s.phone ?? '—' },
{ key: 'national_code', header: 'کد ملی', render: (s) => s.national_code ?? '—' },
{
key: 'active',
header: 'وضعیت',
render: (s) => <ActiveBadge active={s.active} />,
},
{
key: 'created_at',
header: 'تاریخ ثبت',
render: (s) => formatDate(s.created_at),
},
{
key: 'uuid',
header: 'عملیات',
render: (s) => (
<div style={{ display: 'flex', gap: 6 }}>
<button className="btn sm" onClick={() => openEdit(s)} title="ویرایش">
<PencilIcon style={{ width: 15 }} />
</button>
<button
className="btn sm"
onClick={() => setToggleTarget(s)}
title={s.active ? 'غیرفعال‌سازی' : 'فعال‌سازی'}
>
{s.active
? <EyeSlashIcon style={{ width: 15 }} />
: <EyeIcon style={{ width: 15 }} />
}
</button>
</div>
),
},
];
return (
<>
<PageHeader
title="مدیریت پرسنل"
description="لیست پرسنل کلینیک / مطب"
action={
<button className="btn primary sm" onClick={() => setCreateOpen(true)}>
<PlusIcon style={{ width: 16 }} /> افزودن پرسنل
</button>
}
/>
<div className="card">
<DataTable columns={columns} data={staff} loading={isLoading} />
</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 ? 'در حال ذخیره...' : 'ذخیره'}
</button>
<button type="button" className="btn" onClick={() => setCreateOpen(false)}>انصراف</button>
</div>
</form>
</Modal>
{/* Modal ویرایش */}
<Modal open={!!editTarget} onClose={() => setEditTarget(null)} title="ویرایش پرسنل">
<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>
</form>
</Modal>
{/* Confirm toggle */}
<ConfirmDialog
open={!!toggleTarget}
title={toggleTarget?.active ? 'غیرفعال‌سازی پرسنل' : 'فعال‌سازی پرسنل'}
message={`آیا مطمئن هستید که می‌خواهید «${toggleTarget?.full_name}» را ${toggleTarget?.active ? 'غیرفعال' : 'فعال'} کنید؟`}
confirmLabel={toggleTarget?.active ? 'غیرفعال کن' : 'فعال کن'}
onConfirm={() => toggleTarget && toggleMutation.mutate(toggleTarget.uuid)}
onCancel={() => setToggleTarget(null)}
loading={toggleMutation.isPending}
/>
</>
);
}
function StaffFormFields({ form }: { form: ReturnType<typeof useForm<StaffFormData>> }) {
const { register, formState: { errors } } = form;
return (
<div style={{ display: 'flex', flexDirection: 'column', 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 className="field">
<label>تلفن</label>
<input {...register('phone')} placeholder="09121234567" dir="ltr" />
</div>
<div className="field">
<label>کد ملی</label>
<input {...register('national_code')} placeholder="0012345678" dir="ltr" />
</div>
<div className="field">
<label>آدرس</label>
<input {...register('address')} placeholder="آدرس محل سکونت" />
</div>
</div>
);
}