import React, { useEffect, useMemo } from 'react'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; import { DataTable, type DataTableHeader, Table, TableCell, TableContainer, TableBody, TableHead, TableHeader, TableRow, } from '@carbon/react'; import { PatientChartPagination } from '@openmrs/esm-patient-common-lib'; import { formatDatetime, parseDate, useLayoutType, usePagination } from '@openmrs/esm-framework'; import { type Appointment } from '../types'; import { PatientAppointmentsActionMenu } from './patient-appointments-action-menu.component'; import styles from './patient-appointments-table.scss'; const pageSize = 10; interface AppointmentTableProps { patientAppointments: Array; switchedView: boolean; setSwitchedView: (value: boolean) => void; patientUuid: string; } // Updated type to handle the actual API response structure type AppointmentWithExtras = Appointment & { reason?: { person: string; concept: string; value: | string | { uuid: string; display: string; links: Array; }; obsDatetime: string; }; note?: string; location?: { display?: string; name?: string }; appointmentDate?: string; // This is the field the API actually uses appointmentId?: number; }; const PatientAppointmentsTable: React.FC = ({ patientAppointments, patientUuid, switchedView, setSwitchedView, }) => { const { t } = useTranslation(); // FIXED: Create filtered appointments list to exclude cancelled/retired appointments const filteredAppointments = useMemo( () => patientAppointments.filter((a) => a.status !== 'Cancelled' && a.status !== 'RETIRED'), [patientAppointments], ); const { results: paginatedAppointments, currentPage, goTo } = usePagination(filteredAppointments, pageSize); const isTablet = useLayoutType() === 'tablet'; useEffect(() => { if (switchedView && currentPage !== 1) { goTo(1); } }, [switchedView, goTo, currentPage]); const tableHeaders: Array = useMemo( () => [ { key: 'date', header: t('date', 'Date') }, { key: 'reason', header: t('reason', 'Reason') }, { key: 'service', header: t('service', 'Service') }, { key: 'location', header: t('location', 'Location') }, { key: 'notes', header: t('notes', 'Notes') }, ], [t], ); const tableRows = useMemo( () => (paginatedAppointments as AppointmentWithExtras[])?.map((appointment) => { // Helper function to safely convert any value to string const safeStringify = (value: any): string => { if (value === null || value === undefined) { return '——'; } if (typeof value === 'string') { return value; } if (typeof value === 'number') { return value.toString(); } if (typeof value === 'object') { if (value.display) { return value.display; } if (value.name) { return value.name; } return '——'; } return '——'; }; // Handle reason display let reasonValue = '——'; if (appointment.reason?.value) { if (typeof appointment.reason.value === 'string') { reasonValue = appointment.reason.value; } else if (typeof appointment.reason.value === 'object' && appointment.reason.value.display) { reasonValue = appointment.reason.value.display; } } else if (appointment.comments) { reasonValue = safeStringify(appointment.comments); } else if (appointment.note) { reasonValue = safeStringify(appointment.note); } // FIXED: Handle notes display properly - only show actual notes, not fallback to reason const getNotesValue = (appointment: AppointmentWithExtras): string => { // Check for actual notes in the note field const noteValue = appointment.note; if (noteValue && typeof noteValue === 'string' && noteValue.trim() !== '') { return noteValue.trim(); } // Check for actual notes in the comments field const commentsValue = appointment.comments; if (commentsValue && typeof commentsValue === 'string' && commentsValue.trim() !== '') { // Make sure comments is not the same as the reason to avoid duplication const commentsStr = commentsValue.trim(); if (commentsStr !== reasonValue) { return commentsStr; } } // If no actual notes, return "——" return '——'; }; // Use OpenMRS framework date formatting for consistent timezone handling const formatAppointmentDateTime = (appointment: AppointmentWithExtras): string => { try { // Use appointmentDate field first, then fall back to startDateTime const dateToFormat = appointment.appointmentDate || appointment.startDateTime; if (!dateToFormat) { console.warn('No date field found in appointment:', appointment); return '——'; } // Parse the date using OpenMRS parseDate utility // This handles various formats including those with/without timezone info const parsedDate = parseDate(dateToFormat); if (!parsedDate) { console.warn('Failed to parse appointment date:', dateToFormat); return '——'; } // Format using OpenMRS framework's date formatting functions // This ensures consistent timezone handling and locale-aware display return formatDatetime(parsedDate, { mode: 'wide' }); } catch (error) { console.error('Error formatting appointment date:', error); return '——'; } }; return { id: appointment.uuid || appointment.appointmentId, date: formatAppointmentDateTime(appointment), reason: reasonValue, service: safeStringify(appointment.service?.name) || '——', location: safeStringify(appointment.location?.display) || safeStringify(appointment.location?.name) || '——', notes: getNotesValue(appointment), }; }), [paginatedAppointments], ); return (
{({ rows, headers, getHeaderProps, getTableProps }) => ( {headers.map((header) => ( {header.header?.content ?? header.header} ))} {rows.map((row, i) => ( {row.cells.map((cell) => ( {(() => { const value = cell.value?.content ?? cell.value; if (typeof value === 'object' && value !== null) { if (value.display) { return value.display; } if (value.name) { return value.name; } return '——'; } return value || '——'; })()} ))} ))}
)}
{ setSwitchedView(false); goTo(page); }} pageNumber={currentPage} pageSize={pageSize} />
); }; export default PatientAppointmentsTable;