405 lines
11 KiB
TypeScript
405 lines
11 KiB
TypeScript
/*
|
|
* File: patientCareSelectors.ts
|
|
* Description: Redux selectors for patient care state
|
|
* Design & Developed by Tech4Biz Solutions
|
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
|
*/
|
|
|
|
import { createSelector } from '@reduxjs/toolkit';
|
|
import { RootState } from '../../../store';
|
|
import { PatientData } from './patientCareSlice';
|
|
|
|
// ============================================================================
|
|
// BASE SELECTORS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Select Patient Care State
|
|
*
|
|
* Purpose: Get the entire patient care state
|
|
*/
|
|
export const selectPatientCareState = (state: RootState) => state.patientCare;
|
|
|
|
/**
|
|
* Select Patients
|
|
*
|
|
* Purpose: Get the patients array
|
|
*/
|
|
export const selectPatients = (state: RootState) => state.patientCare.patients;
|
|
|
|
/**
|
|
* Select Current Patient
|
|
*
|
|
* Purpose: Get the currently selected patient
|
|
*/
|
|
export const selectCurrentPatient = (state: RootState) => state.patientCare.currentPatient;
|
|
|
|
/**
|
|
* Select Patients Loading State
|
|
*
|
|
* Purpose: Get the loading state for patients
|
|
*/
|
|
export const selectPatientsLoading = (state: RootState) => state.patientCare.isLoading;
|
|
|
|
/**
|
|
* Select Is Refreshing State
|
|
*
|
|
* Purpose: Get the refreshing state for pull-to-refresh
|
|
*/
|
|
export const selectIsRefreshing = (state: RootState) => state.patientCare.isRefreshing;
|
|
|
|
/**
|
|
* Select Patient Details Loading State
|
|
*
|
|
* Purpose: Get the loading state for patient details
|
|
*/
|
|
export const selectPatientDetailsLoading = (state: RootState) => state.patientCare.isLoadingPatientDetails;
|
|
|
|
/**
|
|
* Select Patients Error
|
|
*
|
|
* Purpose: Get the error state for patients
|
|
*/
|
|
export const selectPatientsError = (state: RootState) => state.patientCare.error;
|
|
|
|
/**
|
|
* Select Search Query
|
|
*
|
|
* Purpose: Get the current search query
|
|
*/
|
|
export const selectSearchQuery = (state: RootState) => state.patientCare.searchQuery;
|
|
|
|
/**
|
|
* Select Selected Filter
|
|
*
|
|
* Purpose: Get the currently selected filter
|
|
*/
|
|
export const selectSelectedFilter = (state: RootState) => state.patientCare.selectedFilter;
|
|
|
|
/**
|
|
* Select Sort By
|
|
*
|
|
* Purpose: Get the current sort option
|
|
*/
|
|
export const selectSortBy = (state: RootState) => state.patientCare.sortBy;
|
|
|
|
/**
|
|
* Select Sort Order
|
|
*
|
|
* Purpose: Get the current sort order
|
|
*/
|
|
export const selectSortOrder = (state: RootState) => state.patientCare.sortOrder;
|
|
|
|
/**
|
|
* Select Pagination Info
|
|
*
|
|
* Purpose: Get pagination-related state
|
|
*/
|
|
export const selectPaginationInfo = (state: RootState) => ({
|
|
currentPage: state.patientCare.currentPage,
|
|
itemsPerPage: state.patientCare.itemsPerPage,
|
|
totalItems: state.patientCare.totalItems,
|
|
});
|
|
|
|
/**
|
|
* Select Last Updated
|
|
*
|
|
* Purpose: Get the last updated timestamp
|
|
*/
|
|
export const selectLastUpdated = (state: RootState) => state.patientCare.lastUpdated;
|
|
|
|
// ============================================================================
|
|
// COMPUTED SELECTORS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Select Filtered Patients
|
|
*
|
|
* Purpose: Get patients filtered by search query and selected filter
|
|
*/
|
|
export const selectFilteredPatients = createSelector(
|
|
[selectPatients, selectSearchQuery, selectSelectedFilter, selectSortBy, selectSortOrder],
|
|
(patients, searchQuery, selectedFilter, sortBy, sortOrder) => {
|
|
// Ensure patients is always an array
|
|
if (!patients || !Array.isArray(patients)) {
|
|
return [];
|
|
}
|
|
|
|
let filteredPatients = [...patients];
|
|
|
|
// Apply filter based on processing status
|
|
if (selectedFilter !== 'all') {
|
|
filteredPatients = filteredPatients.filter((patient: PatientData) => {
|
|
const status = patient.patient_info.status.toLowerCase();
|
|
return status === selectedFilter;
|
|
});
|
|
}
|
|
|
|
// Apply search
|
|
if (searchQuery.trim()) {
|
|
const query = searchQuery.toLowerCase().trim();
|
|
filteredPatients = filteredPatients.filter((patient: PatientData) => {
|
|
const patientInfo = patient.patient_info;
|
|
|
|
const name = (patientInfo.name || '').toLowerCase();
|
|
const patId = (patient.patid || '').toLowerCase();
|
|
const institution = (patientInfo.institution || '').toLowerCase();
|
|
const modality = (patientInfo.modality || '').toLowerCase();
|
|
|
|
return (
|
|
name.includes(query) ||
|
|
patId.includes(query) ||
|
|
institution.includes(query) ||
|
|
modality.includes(query)
|
|
);
|
|
});
|
|
}
|
|
|
|
// Apply sorting
|
|
filteredPatients.sort((a: PatientData, b: PatientData) => {
|
|
let aValue: any;
|
|
let bValue: any;
|
|
|
|
switch (sortBy) {
|
|
case 'name':
|
|
aValue = (a.patient_info.name || '').toLowerCase();
|
|
bValue = (b.patient_info.name || '').toLowerCase();
|
|
break;
|
|
case 'processed':
|
|
aValue = new Date(a.last_processed_at).getTime();
|
|
bValue = new Date(b.last_processed_at).getTime();
|
|
break;
|
|
case 'date':
|
|
default:
|
|
aValue = new Date(a.patient_info.date).getTime();
|
|
bValue = new Date(b.patient_info.date).getTime();
|
|
break;
|
|
}
|
|
|
|
if (aValue < bValue) {
|
|
return sortOrder === 'asc' ? -1 : 1;
|
|
}
|
|
if (aValue > bValue) {
|
|
return sortOrder === 'asc' ? 1 : -1;
|
|
}
|
|
return 0;
|
|
});
|
|
|
|
return filteredPatients;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Select Processed Patients
|
|
*
|
|
* Purpose: Get patients with processed status
|
|
*/
|
|
export const selectProcessedPatients = createSelector(
|
|
[selectPatients],
|
|
(patients) => {
|
|
if (!patients || !Array.isArray(patients)) return [];
|
|
return patients.filter((patient: PatientData) =>
|
|
patient.patient_info.status.toLowerCase() === 'processed'
|
|
);
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Select Pending Patients
|
|
*
|
|
* Purpose: Get patients with pending status
|
|
*/
|
|
export const selectPendingPatients = createSelector(
|
|
[selectPatients],
|
|
(patients) => {
|
|
if (!patients || !Array.isArray(patients)) return [];
|
|
return patients.filter((patient: PatientData) =>
|
|
patient.patient_info.status.toLowerCase() === 'pending'
|
|
);
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Select Error Patients
|
|
*
|
|
* Purpose: Get patients with error status
|
|
*/
|
|
export const selectErrorPatients = createSelector(
|
|
[selectPatients],
|
|
(patients) => {
|
|
if (!patients || !Array.isArray(patients)) return [];
|
|
return patients.filter((patient: PatientData) =>
|
|
patient.patient_info.status.toLowerCase() === 'error'
|
|
);
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Select Patients by Modality
|
|
*
|
|
* Purpose: Get patients grouped by imaging modality
|
|
*/
|
|
export const selectPatientsByModality = createSelector(
|
|
[selectPatients],
|
|
(patients) => {
|
|
if (!patients || !Array.isArray(patients)) return {};
|
|
|
|
const grouped: { [key: string]: PatientData[] } = {};
|
|
|
|
patients.forEach((patient: PatientData) => {
|
|
const modality = patient.patient_info.modality || 'Unknown';
|
|
if (!grouped[modality]) {
|
|
grouped[modality] = [];
|
|
}
|
|
grouped[modality].push(patient);
|
|
});
|
|
|
|
return grouped;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Select Patient Statistics
|
|
*
|
|
* Purpose: Get statistics about patients
|
|
*/
|
|
export const selectPatientStats = createSelector(
|
|
[selectPatients],
|
|
(patients) => {
|
|
if (!patients || !Array.isArray(patients)) {
|
|
return {
|
|
total: 0,
|
|
processed: 0,
|
|
pending: 0,
|
|
averageAge: 0,
|
|
modalities: {},
|
|
totalFiles: 0,
|
|
processedPercentage: 0,
|
|
pendingPercentage: 0,
|
|
};
|
|
}
|
|
|
|
const total = patients.length;
|
|
const processed = patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'processed').length;
|
|
const pending = patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'pending').length;
|
|
|
|
// Calculate average age
|
|
const totalAge = patients.reduce((sum: number, patient: PatientData) => {
|
|
const age = parseInt(patient.patient_info.age) || 0;
|
|
return sum + age;
|
|
}, 0);
|
|
const averageAge = total > 0 ? Math.round(totalAge / total) : 0;
|
|
|
|
// Modality distribution
|
|
const modalities: { [key: string]: number } = {};
|
|
patients.forEach((patient: PatientData) => {
|
|
const modality = patient.patient_info.modality || 'Unknown';
|
|
modalities[modality] = (modalities[modality] || 0) + 1;
|
|
});
|
|
|
|
// Total files processed
|
|
const totalFiles = patients.reduce((sum: number, patient: PatientData) => sum + (patient.total_files_processed || 0), 0);
|
|
|
|
return {
|
|
total,
|
|
processed,
|
|
pending,
|
|
averageAge,
|
|
modalities,
|
|
totalFiles,
|
|
processedPercentage: total > 0 ? Math.round((processed / total) * 100) : 0,
|
|
pendingPercentage: total > 0 ? Math.round((pending / total) * 100) : 0,
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Select Patient by ID
|
|
*
|
|
* Purpose: Get a specific patient by ID
|
|
*
|
|
* @param patientId - The ID of the patient to find
|
|
*/
|
|
export const selectPatientById = (patientId: string) =>
|
|
createSelector(
|
|
[selectPatients],
|
|
(patients) => {
|
|
if (!patients || !Array.isArray(patients)) return undefined;
|
|
return patients.find((patient: PatientData) => patient.patid === patientId);
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Select Patients Need Attention
|
|
*
|
|
* Purpose: Get patients that need immediate attention
|
|
*/
|
|
export const selectPatientsNeedAttention = createSelector(
|
|
[selectPatients],
|
|
(patients) => {
|
|
if (!patients || !Array.isArray(patients)) return [];
|
|
|
|
return patients.filter((patient: PatientData) => {
|
|
// Error patients always need attention
|
|
if (patient.patient_info.status.toLowerCase() === 'error') return true;
|
|
|
|
// Patients with critical report status
|
|
if (patient.patient_info.report_status.toLowerCase() === 'critical') return true;
|
|
|
|
// Patients with high frame count (complex cases)
|
|
if (patient.patient_info.frame_count > 100) return true;
|
|
|
|
// Patients with multiple series (complex cases)
|
|
if (patient.series_summary.length > 5) return true;
|
|
|
|
return false;
|
|
});
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Select Has Data
|
|
*
|
|
* Purpose: Check if we have patient data
|
|
*/
|
|
export const selectHasPatientData = createSelector(
|
|
[selectPatients],
|
|
(patients) => patients && Array.isArray(patients) && patients.length > 0
|
|
);
|
|
|
|
/**
|
|
* Select Is Empty State
|
|
*
|
|
* Purpose: Check if we should show empty state
|
|
*/
|
|
export const selectIsEmptyState = createSelector(
|
|
[selectPatients, selectPatientsLoading, selectFilteredPatients],
|
|
(patients, isLoading, filteredPatients) =>
|
|
!isLoading && patients && Array.isArray(patients) && patients.length > 0 && filteredPatients.length === 0
|
|
);
|
|
|
|
/**
|
|
* Select Patient Counts for Filters
|
|
*
|
|
* Purpose: Get patient counts for each filter category
|
|
*/
|
|
export const selectPatientCounts = createSelector(
|
|
[selectPatients],
|
|
(patients) => {
|
|
if (!patients || !Array.isArray(patients)) {
|
|
return { all: 0, processed: 0, pending: 0 };
|
|
}
|
|
|
|
return {
|
|
all: patients.length,
|
|
processed: patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'processed').length,
|
|
pending: patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'pending').length,
|
|
};
|
|
}
|
|
);
|
|
|
|
/*
|
|
* End of File: patientCareSelectors.ts
|
|
* Design & Developed by Tech4Biz Solutions
|
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
|
*/ |