feat(appointments): edit page + filters modal — phase C2/C3

- AppointmentEditPage (edit.pdf): full-page edit of بخش/سرویس/پرسنل, Jalali
  date + start/end time, deposit, status and notes; hydrates from
  GET /appointment/{uuid} and saves through the general PATCH with the
  optimistic-lock version. Routed at /admin/appointments/:uuid/edit (the
  actions-menu ویرایش target).
- AppointmentFiltersModal (filter-desktop.pdf): name/national-code search,
  بخش/سرویس selects, six status checkboxes (لغو شده covers both cancel
  reasons), gender radios, حذف همه reset. Filtering is client-side over the
  loaded day via the pure applyAppointmentFilters; toolbar gains the filter
  button with an active indicator.
- /my/appointments rows now include patient_national_code and patient_gender
  so the filters have data to match on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-14 00:02:22 +03:30
co-authored by Claude Fable 5
parent 91ea1b02aa
commit f736e4acd8
8 changed files with 493 additions and 1 deletions
+27 -1
View File
@@ -13,6 +13,9 @@ import { useAuthStore } from '../stores/authStore';
import AppointmentStatusDropdown from '../components/ui/AppointmentStatusDropdown';
import AppointmentActionsMenu from '../components/AppointmentActions';
import NewAppointmentDrawer from '../components/NewAppointmentDrawer';
import AppointmentFiltersModal, { applyAppointmentFilters, EMPTY_FILTERS } from '../components/AppointmentFiltersModal';
import type { AppointmentFilters } from '../components/AppointmentFiltersModal';
import { AdjustmentsHorizontalIcon } from '@heroicons/react/24/outline';
import PersianCalendar from '../components/ui/PersianCalendar';
import SearchableSelect from '../components/ui/SearchableSelect';
@@ -549,6 +552,8 @@ export default function AppointmentsPage() {
const [bookingSlot, setBookingSlot] = useState<BookingSlot | null>(null);
const [bookingHint, setBookingHint] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false);
const [filtersOpen, setFiltersOpen] = useState(false);
const [filters, setFilters] = useState<AppointmentFilters>(EMPTY_FILTERS);
const qc = useQueryClient();
// ── Appointments query
@@ -566,6 +571,8 @@ export default function AppointmentsPage() {
queryFn: () => api.get(`${apptEndpoint}?${apptParams}`),
});
const appointments: Appointment[] = apptQuery.data?.data ?? EMPTY_ARR;
const filteredAppointments = applyAppointmentFilters(appointments, filters);
const filtersActive = filters !== EMPTY_FILTERS && JSON.stringify(filters) !== JSON.stringify(EMPTY_FILTERS);
// ── Clinic: load doctors from clinic profile (not derived from appointments)
const clinicDoctorsQuery = useQuery<ApiResponse<{ data: { uuid: string; name: string }[] }>>({
@@ -705,6 +712,20 @@ export default function AppointmentsPage() {
</button>
)}
{/* Filters (Figma فیلترها) */}
<button
aria-label="فیلترها"
className="btn sm"
onClick={() => setFiltersOpen(true)}
style={{
border: `1px solid ${filtersActive ? 'var(--primary)' : 'var(--border)'}`,
color: filtersActive ? 'var(--primary)' : 'var(--text-2)',
background: 'var(--surface)',
}}
>
<AdjustmentsHorizontalIcon style={{ width: 16 }} />
</button>
{/* View toggle */}
<div style={{
display: 'flex', background: 'var(--surface-2)', borderRadius: 'var(--r-sm)',
@@ -759,7 +780,7 @@ export default function AppointmentsPage() {
<div style={{ padding: 16 }}>
{viewMode === 'table' ? (
<TableView
items={appointments}
items={filteredAppointments}
loading={apptQuery.isLoading}
queryKey={apptQueryKey}
showDoctor={showDoctorCol}
@@ -807,6 +828,11 @@ export default function AppointmentsPage() {
/>
)}
{/* Filters modal */}
{filtersOpen && (
<AppointmentFiltersModal value={filters} onApply={setFilters} onClose={() => setFiltersOpen(false)} />
)}
{/* Rich create drawer (اضافه کردن نوبت جدید) */}
{drawerOpen && (
<NewAppointmentDrawer