NeoScan_Radiologist/app/modules/PatientCare/redux/patientCareSlice.ts

520 lines
14 KiB
TypeScript

/*
* File: patientCareSlice.ts
* Description: Patient care state management slice
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { patientAPI } from '../services/patientAPI';
// ============================================================================
// TYPES
// ============================================================================
/**
* New API Response Types
*/
export interface SeriesSummary {
series_num: string;
series_description: string;
total_images: number;
png_preview: string;
modality: string;
}
export interface PatientInfo {
name: string;
age: string;
sex: string;
date: string;
institution: string;
modality: string;
status: string;
report_status: string;
file_name: string;
file_type: string;
frame_count: number;
}
export interface PatientData {
patid: string;
hospital_id: string;
first_processed_at: string;
last_processed_at: string;
total_files_processed: number;
patient_info: PatientInfo;
series_summary: SeriesSummary[];
processing_metadata: any;
}
export interface PatientCareState {
// Patients data
patients: PatientData[];
currentPatient: PatientData | null;
// Loading states
isLoading: boolean;
isRefreshing: boolean;
isLoadingPatientDetails: boolean;
// Error handling
error: string | null;
// Search and filtering
searchQuery: string;
selectedFilter: 'all' | 'processed' | 'pending' | 'error';
sortBy: 'date' | 'name' | 'processed';
sortOrder: 'asc' | 'desc';
// Pagination
currentPage: number;
itemsPerPage: number;
totalItems: number;
// Cache
lastUpdated: string | null;
cacheExpiry: string | null;
}
// ============================================================================
// ASYNC THUNKS
// ============================================================================
/**
* Fetch Patients Async Thunk
*
* Purpose: Fetch patients list from API
*
* @returns Promise with patients data or error
*/
export const fetchPatients = createAsyncThunk(
'patientCare/fetchPatients',
async (token: string, { rejectWithValue }) => {
try {
const response: any = await patientAPI.getPatients(token);
if (response.ok && response.data&& response.data.data) {
// Return the patients data directly from the new API structure
return response.data.data as PatientData[];
} else {
// Fallback to mock data for development
const mockPatients: PatientData[] = [
{
patid: "demo001",
hospital_id: "demo-hospital-001",
first_processed_at: "2025-01-15T10:30:00Z",
last_processed_at: "2025-01-15T11:45:00Z",
total_files_processed: 3,
patient_info: {
name: "John Doe",
age: "38",
sex: "M",
date: "2025-01-15",
institution: "City General Hospital",
modality: "CT",
status: "Processed",
report_status: "Available",
file_name: "chest_ct_001.dcm",
file_type: "dcm",
frame_count: 50
},
series_summary: [
{
series_num: "1",
series_description: "Chest CT",
total_images: 50,
png_preview: "/images/ct_chest_1.png",
modality: "CT"
}
],
processing_metadata: {}
},
{
patid: "demo002",
hospital_id: "demo-hospital-002",
first_processed_at: "2025-01-15T09:15:00Z",
last_processed_at: "2025-01-15T10:30:00Z",
total_files_processed: 2,
patient_info: {
name: "Jane Smith",
age: "33",
sex: "F",
date: "2025-01-15",
institution: "Memorial Medical Center",
modality: "MR",
status: "Processed",
report_status: "Available",
file_name: "brain_mri_001.dcm",
file_type: "dcm",
frame_count: 120
},
series_summary: [
{
series_num: "1",
series_description: "Brain MRI",
total_images: 120,
png_preview: "/images/mri_brain_1.png",
modality: "MR"
}
],
processing_metadata: {}
}
];
return [];
}
} catch (error: any) {
console.error('Fetch patients error:', error);
return rejectWithValue(error.message || 'Failed to fetch patients.');
}
}
);
/**
* Fetch Patient Details Async Thunk
*
* Purpose: Fetch detailed patient information
*
* @param patientId - ID of the patient to fetch
* @returns Promise with patient details or error
*/
export const fetchPatientDetails = createAsyncThunk(
'patientCare/fetchPatientDetails',
async (patientId: string, { rejectWithValue }) => {
try {
// TODO: Replace with actual API call
await new Promise((resolve) => setTimeout(resolve as any, 1000));
// Mock patient details for specific patient
const mockPatient: PatientData = {
patid: patientId,
hospital_id: `demo-hospital-${patientId}`,
first_processed_at: "2025-01-15T10:30:00Z",
last_processed_at: "2025-01-15T11:45:00Z",
total_files_processed: 3,
patient_info: {
name: `Patient ${patientId}`,
age: "38",
sex: "M",
date: "2025-01-15",
institution: "City General Hospital",
modality: "CT",
status: "Processed",
report_status: "Available",
file_name: `patient_${patientId}.dcm`,
file_type: "dcm",
frame_count: 50
},
series_summary: [
{
series_num: "1",
series_description: "Chest CT",
total_images: 50,
png_preview: `/images/ct_chest_${patientId}.png`,
modality: "CT"
}
],
processing_metadata: {}
};
return mockPatient;
} catch (error) {
return rejectWithValue('Failed to fetch patient details.');
}
}
);
/**
* Update Patient Async Thunk
*
* Purpose: Update patient information
*
* @param patientData - Updated patient data
* @returns Promise with updated patient or error
*/
export const updatePatient = createAsyncThunk(
'patientCare/updatePatient',
async (patientData: Partial<PatientData> & { patid: string }, { rejectWithValue }) => {
try {
// TODO: Replace with actual API call
await new Promise((resolve) => setTimeout(resolve as any, 800));
return patientData;
} catch (error) {
return rejectWithValue('Failed to update patient.');
}
}
);
// ============================================================================
// INITIAL STATE
// ============================================================================
/**
* Initial Patient Care State
*
* Purpose: Define the initial state for patient care
*
* Features:
* - Patients list and management
* - Current patient details
* - Loading states for async operations
* - Error handling and messages
* - Search and filtering
* - Pagination and caching
*/
const initialState: PatientCareState = {
// Patients data
patients: [],
currentPatient: null,
// Loading states
isLoading: false,
isRefreshing: false,
isLoadingPatientDetails: false,
// Error handling
error: null,
// Search and filtering
searchQuery: '',
selectedFilter: 'all',
sortBy: 'date',
sortOrder: 'desc',
// Pagination
currentPage: 1,
itemsPerPage: 20,
totalItems: 0,
// Cache
lastUpdated: null,
cacheExpiry: null,
};
// ============================================================================
// PATIENT CARE SLICE
// ============================================================================
/**
* Patient Care Slice
*
* Purpose: Redux slice for patient care state management
*
* Features:
* - Patient data management
* - Search and filtering
* - Pagination
* - Caching
* - Error handling
* - Loading states
*/
const patientCareSlice = createSlice({
name: 'patientCare',
initialState,
reducers: {
/**
* Clear Error Action
*
* Purpose: Clear patient care errors
*/
clearError: (state) => {
state.error = null;
},
/**
* Set Search Query Action
*
* Purpose: Set search query for patients
*/
setSearchQuery: (state, action: PayloadAction<string>) => {
state.searchQuery = action.payload;
state.currentPage = 1; // Reset to first page when searching
},
/**
* Set Filter Action
*
* Purpose: Set patient filter
*/
setFilter: (state, action: PayloadAction<'all' | 'processed' | 'pending' | 'error'>) => {
state.selectedFilter = action.payload;
state.currentPage = 1; // Reset to first page when filtering
},
/**
* Set Sort Action
*
* Purpose: Set patient sort options
*/
setSort: (state, action: PayloadAction<{ by: 'date' | 'name' | 'processed'; order: 'asc' | 'desc' }>) => {
state.sortBy = action.payload.by;
state.sortOrder = action.payload.order;
},
/**
* Set Current Page Action
*
* Purpose: Set current page for pagination
*/
setCurrentPage: (state, action: PayloadAction<number>) => {
state.currentPage = action.payload;
},
/**
* Set Items Per Page Action
*
* Purpose: Set items per page for pagination
*/
setItemsPerPage: (state, action: PayloadAction<number>) => {
state.itemsPerPage = action.payload;
state.currentPage = 1; // Reset to first page when changing items per page
},
/**
* Set Current Patient Action
*
* Purpose: Set the currently selected patient
*/
setCurrentPatient: (state, action: PayloadAction<PatientData | null>) => {
state.currentPatient = action.payload;
},
/**
* Update Patient in List Action
*
* Purpose: Update a patient in the patients list
*/
updatePatientInList: (state, action: PayloadAction<PatientData>) => {
const index = state.patients.findIndex(patient => patient.patid === action.payload.patid);
if (index !== -1) {
state.patients[index] = action.payload;
}
// Update current patient if it's the same patient
if (state.currentPatient && state.currentPatient.patid === action.payload.patid) {
state.currentPatient = action.payload;
}
},
/**
* Add Patient Action
*
* Purpose: Add a new patient to the list
*/
addPatient: (state, action: PayloadAction<PatientData>) => {
state.patients.unshift(action.payload);
state.totalItems += 1;
},
/**
* Remove Patient Action
*
* Purpose: Remove a patient from the list
*/
removePatient: (state, action: PayloadAction<string>) => {
const index = state.patients.findIndex(patient => patient.patid === action.payload);
if (index !== -1) {
state.patients.splice(index, 1);
state.totalItems -= 1;
}
// Clear current patient if it's the same patient
if (state.currentPatient && state.currentPatient.patid === action.payload) {
state.currentPatient = null;
}
},
/**
* Clear Cache Action
*
* Purpose: Clear patient data cache
*/
clearCache: (state) => {
state.patients = [];
state.currentPatient = null;
state.lastUpdated = null;
state.cacheExpiry = null;
},
},
extraReducers: (builder) => {
// Fetch Patients
builder
.addCase(fetchPatients.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(fetchPatients.fulfilled, (state, action) => {
state.isLoading = false;
state.patients = action.payload;
state.totalItems = action.payload.length;
state.lastUpdated = new Date().toLocaleDateString();
state.cacheExpiry = new Date(Date.now() + 5 * 60 * 1000).toLocaleDateString(); // 5 minutes
state.error = null;
})
.addCase(fetchPatients.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
});
// Fetch Patient Details
builder
.addCase(fetchPatientDetails.pending, (state) => {
state.isLoadingPatientDetails = true;
state.error = null;
})
.addCase(fetchPatientDetails.fulfilled, (state, action) => {
state.isLoadingPatientDetails = false;
state.currentPatient = action.payload;
state.error = null;
})
.addCase(fetchPatientDetails.rejected, (state, action) => {
state.isLoadingPatientDetails = false;
state.error = action.payload as string;
});
// Update Patient
builder
.addCase(updatePatient.fulfilled, (state, action) => {
// Update patient in list
const index = state.patients.findIndex(patient => patient.patid === action.payload.patid);
if (index !== -1) {
state.patients[index] = { ...state.patients[index], ...action.payload };
}
// Update current patient if it's the same patient
if (state.currentPatient && state.currentPatient.patid === action.payload.patid) {
state.currentPatient = { ...state.currentPatient, ...action.payload };
}
})
.addCase(updatePatient.rejected, (state, action) => {
state.error = action.payload as string;
});
},
});
// ============================================================================
// EXPORTS
// ============================================================================
export const {
clearError,
setSearchQuery,
setFilter,
setSort,
setCurrentPage,
setItemsPerPage,
setCurrentPatient,
updatePatientInList,
addPatient,
removePatient,
clearCache,
} = patientCareSlice.actions;
export default patientCareSlice.reducer;
/*
* End of File: patientCareSlice.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/