622 lines
18 KiB
TypeScript
622 lines
18 KiB
TypeScript
/*
|
|
* File: aiPredictionSlice.ts
|
|
* Description: Redux slice for AI Prediction state management
|
|
* Design & Developed by Tech4Biz Solutions
|
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
|
*/
|
|
|
|
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
|
import {
|
|
AIPredictionCase,
|
|
AIPredictionState,
|
|
AIPredictionStats,
|
|
AIPredictionAPIResponse
|
|
} from '../types';
|
|
import { aiPredictionAPI } from '../services';
|
|
|
|
// ============================================================================
|
|
// ASYNC THUNKS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Fetch AI Predictions Async Thunk
|
|
*
|
|
* Purpose: Fetch AI prediction results from API
|
|
*
|
|
* @param token - Authentication token
|
|
* @param params - Optional query parameters for filtering
|
|
* @returns Promise with AI prediction data or error
|
|
*/
|
|
export const fetchAIPredictions = createAsyncThunk(
|
|
'aiPrediction/fetchAIPredictions',
|
|
async (payload: {
|
|
token: string;
|
|
params?: {
|
|
page?: number;
|
|
limit?: number;
|
|
urgency?: string;
|
|
severity?: string;
|
|
category?: string;
|
|
search?: string;
|
|
}
|
|
}, { rejectWithValue }) => {
|
|
try {
|
|
const response: any = await aiPredictionAPI.getAllPredictions(payload.token, payload.params);
|
|
console.log('AI predictions response:', response);
|
|
|
|
if (response.ok && response.data && response.data.success) {
|
|
// Add additional metadata to each case for UI purposes
|
|
const enhancedCases = response.data.data.map((aiCase: AIPredictionCase) => ({
|
|
...aiCase,
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
review_status: 'pending' as const,
|
|
priority: getPriorityFromPrediction(aiCase.prediction)
|
|
}));
|
|
|
|
console.log('Enhanced AI prediction cases:', enhancedCases);
|
|
return {
|
|
cases: enhancedCases as AIPredictionCase[],
|
|
total: response.data.total || enhancedCases.length,
|
|
page: response.data.page || 1,
|
|
limit: response.data.limit || 20
|
|
};
|
|
} else {
|
|
// Fallback to mock data for development
|
|
const mockData = generateMockAIPredictions();
|
|
return {
|
|
cases: mockData,
|
|
total: mockData.length,
|
|
page: 1,
|
|
limit: 20
|
|
};
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Fetch AI predictions error:', error);
|
|
return rejectWithValue(error.message || 'Failed to fetch AI predictions.');
|
|
}
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Fetch AI Prediction Case Details Async Thunk
|
|
*
|
|
* Purpose: Fetch detailed information for a specific AI prediction case
|
|
*
|
|
* @param caseId - AI prediction case ID
|
|
* @param token - Authentication token
|
|
* @returns Promise with case details or error
|
|
*/
|
|
export const fetchAIPredictionDetails = createAsyncThunk(
|
|
'aiPrediction/fetchAIPredictionDetails',
|
|
async (payload: { caseId: string; token: string }, { rejectWithValue }) => {
|
|
try {
|
|
const response: any = await aiPredictionAPI.getCaseDetails(payload.caseId, payload.token);
|
|
|
|
if (response.ok && response.data) {
|
|
return response.data as AIPredictionCase;
|
|
} else {
|
|
// Fallback to mock data
|
|
const mockCase = generateMockAIPredictions().find(c => c.patid === payload.caseId);
|
|
if (mockCase) {
|
|
return mockCase;
|
|
}
|
|
throw new Error('Case not found');
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Fetch AI prediction details error:', error);
|
|
return rejectWithValue(error.message || 'Failed to fetch case details.');
|
|
}
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Update Case Review Async Thunk
|
|
*
|
|
* Purpose: Update review status of an AI prediction case
|
|
*
|
|
* @param caseId - Case ID to update
|
|
* @param reviewData - Review data
|
|
* @param token - Authentication token
|
|
* @returns Promise with updated case or error
|
|
*/
|
|
export const updateCaseReview = createAsyncThunk(
|
|
'aiPrediction/updateCaseReview',
|
|
async (payload: {
|
|
caseId: string;
|
|
reviewData: {
|
|
review_status: 'pending' | 'reviewed' | 'confirmed' | 'disputed';
|
|
reviewed_by?: string;
|
|
review_notes?: string;
|
|
priority?: 'critical' | 'high' | 'medium' | 'low';
|
|
};
|
|
token: string;
|
|
}, { rejectWithValue }) => {
|
|
try {
|
|
const response: any = await aiPredictionAPI.updateCaseReview(
|
|
payload.caseId,
|
|
payload.reviewData,
|
|
payload.token
|
|
);
|
|
|
|
if (response.ok && response.data) {
|
|
return {
|
|
caseId: payload.caseId,
|
|
...payload.reviewData,
|
|
updated_at: new Date().toISOString()
|
|
};
|
|
} else {
|
|
throw new Error('Failed to update case review');
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Update case review error:', error);
|
|
return rejectWithValue(error.message || 'Failed to update case review.');
|
|
}
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Fetch AI Prediction Statistics Async Thunk
|
|
*
|
|
* Purpose: Fetch statistics for AI predictions dashboard
|
|
*
|
|
* @param token - Authentication token
|
|
* @param timeRange - Time range filter
|
|
* @returns Promise with statistics data or error
|
|
*/
|
|
export const fetchAIPredictionStats = createAsyncThunk(
|
|
'aiPrediction/fetchAIPredictionStats',
|
|
async (payload: { token: string; timeRange?: 'today' | 'week' | 'month' }, { rejectWithValue }) => {
|
|
try {
|
|
const response: any = await aiPredictionAPI.getPredictionStats(payload.token, payload.timeRange);
|
|
|
|
if (response.ok && response.data) {
|
|
return response.data as AIPredictionStats;
|
|
} else {
|
|
// Fallback to mock stats
|
|
return generateMockStats();
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Fetch AI prediction stats error:', error);
|
|
return rejectWithValue(error.message || 'Failed to fetch statistics.');
|
|
}
|
|
}
|
|
);
|
|
|
|
// ============================================================================
|
|
// HELPER FUNCTIONS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Get Priority from AI Prediction
|
|
*
|
|
* Purpose: Determine case priority based on AI prediction results
|
|
*/
|
|
function getPriorityFromPrediction(prediction: any): 'critical' | 'high' | 'medium' | 'low' {
|
|
if (prediction.clinical_urgency === 'emergency' || prediction.primary_severity === 'high') {
|
|
return 'critical';
|
|
}
|
|
if (prediction.clinical_urgency === 'urgent' || prediction.primary_severity === 'medium') {
|
|
return 'high';
|
|
}
|
|
if (prediction.clinical_urgency === 'moderate' || prediction.primary_severity === 'low') {
|
|
return 'medium';
|
|
}
|
|
return 'low';
|
|
}
|
|
|
|
/**
|
|
* Generate Mock AI Predictions
|
|
*
|
|
* Purpose: Generate mock data for development and testing
|
|
*/
|
|
function generateMockAIPredictions(): AIPredictionCase[] {
|
|
return [
|
|
{
|
|
patid: "demogw05-08-2017",
|
|
hospital_id: "eec24855-d8ae-4fad-8e54-af0480343dc2",
|
|
prediction: {
|
|
label: "midline shift",
|
|
finding_type: "pathology",
|
|
clinical_urgency: "urgent",
|
|
confidence_score: 0.996,
|
|
finding_category: "abnormal",
|
|
primary_severity: "high",
|
|
anatomical_location: "brain"
|
|
},
|
|
created_at: "2024-01-15T10:30:00Z",
|
|
updated_at: "2024-01-15T10:30:00Z",
|
|
review_status: "pending",
|
|
priority: "critical"
|
|
},
|
|
{
|
|
patid: "demo-patient-002",
|
|
hospital_id: "eec24855-d8ae-4fad-8e54-af0480343dc2",
|
|
prediction: {
|
|
label: "normal brain",
|
|
finding_type: "no_pathology",
|
|
clinical_urgency: "routine",
|
|
confidence_score: 0.892,
|
|
finding_category: "normal",
|
|
primary_severity: "none",
|
|
anatomical_location: "not_applicable"
|
|
},
|
|
created_at: "2024-01-15T09:15:00Z",
|
|
updated_at: "2024-01-15T09:15:00Z",
|
|
review_status: "reviewed",
|
|
priority: "low"
|
|
},
|
|
{
|
|
patid: "demo-patient-003",
|
|
hospital_id: "eec24855-d8ae-4fad-8e54-af0480343dc2",
|
|
prediction: {
|
|
label: "hemorrhage",
|
|
finding_type: "pathology",
|
|
clinical_urgency: "emergency",
|
|
confidence_score: 0.945,
|
|
finding_category: "critical",
|
|
primary_severity: "high",
|
|
anatomical_location: "temporal lobe"
|
|
},
|
|
created_at: "2024-01-15T11:45:00Z",
|
|
updated_at: "2024-01-15T11:45:00Z",
|
|
review_status: "confirmed",
|
|
priority: "critical"
|
|
}
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Generate Mock Statistics
|
|
*
|
|
* Purpose: Generate mock statistics for development
|
|
*/
|
|
function generateMockStats(): AIPredictionStats {
|
|
return {
|
|
totalCases: 156,
|
|
criticalCases: 23,
|
|
urgentCases: 45,
|
|
reviewedCases: 89,
|
|
pendingCases: 67,
|
|
averageConfidence: 0.887,
|
|
todaysCases: 12,
|
|
weeklyTrend: 15.4
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// INITIAL STATE
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Initial AI Prediction State
|
|
*
|
|
* Purpose: Define the initial state for AI predictions
|
|
*
|
|
* Features:
|
|
* - Prediction cases list and management
|
|
* - Current case details
|
|
* - Loading states for async operations
|
|
* - Error handling and messages
|
|
* - Search and filtering
|
|
* - Pagination support
|
|
* - Cache management
|
|
*/
|
|
const initialState: AIPredictionState = {
|
|
// Prediction data
|
|
predictionCases: [],
|
|
currentCase: null,
|
|
|
|
// Loading states
|
|
isLoading: false,
|
|
isRefreshing: false,
|
|
isLoadingCaseDetails: false,
|
|
|
|
// Error handling
|
|
error: null,
|
|
|
|
// Search and filtering
|
|
searchQuery: '',
|
|
selectedUrgencyFilter: 'all',
|
|
selectedSeverityFilter: 'all',
|
|
selectedCategoryFilter: 'all',
|
|
sortBy: 'date',
|
|
sortOrder: 'desc',
|
|
|
|
// Pagination
|
|
currentPage: 1,
|
|
itemsPerPage: 20,
|
|
totalItems: 0,
|
|
|
|
// Cache management
|
|
lastUpdated: null,
|
|
cacheExpiry: null,
|
|
|
|
// UI state
|
|
showFilters: false,
|
|
selectedCaseIds: [],
|
|
};
|
|
|
|
// ============================================================================
|
|
// AI PREDICTION SLICE
|
|
// ============================================================================
|
|
|
|
/**
|
|
* AI Prediction Slice
|
|
*
|
|
* Purpose: Redux slice for AI prediction state management
|
|
*
|
|
* Features:
|
|
* - AI prediction data management
|
|
* - Search and filtering
|
|
* - Case review management
|
|
* - Pagination
|
|
* - Caching
|
|
* - Error handling
|
|
* - Loading states
|
|
*/
|
|
const aiPredictionSlice = createSlice({
|
|
name: 'aiPrediction',
|
|
initialState,
|
|
reducers: {
|
|
/**
|
|
* Clear Error Action
|
|
*
|
|
* Purpose: Clear AI prediction errors
|
|
*/
|
|
clearError: (state) => {
|
|
state.error = null;
|
|
},
|
|
|
|
/**
|
|
* Set Search Query Action
|
|
*
|
|
* Purpose: Set search query for AI predictions
|
|
*/
|
|
setSearchQuery: (state, action: PayloadAction<string>) => {
|
|
state.searchQuery = action.payload;
|
|
state.currentPage = 1; // Reset to first page when searching
|
|
},
|
|
|
|
/**
|
|
* Set Urgency Filter Action
|
|
*
|
|
* Purpose: Set urgency filter for AI predictions
|
|
*/
|
|
setUrgencyFilter: (state, action: PayloadAction<AIPredictionState['selectedUrgencyFilter']>) => {
|
|
state.selectedUrgencyFilter = action.payload;
|
|
state.currentPage = 1; // Reset to first page when filtering
|
|
},
|
|
|
|
/**
|
|
* Set Severity Filter Action
|
|
*
|
|
* Purpose: Set severity filter for AI predictions
|
|
*/
|
|
setSeverityFilter: (state, action: PayloadAction<AIPredictionState['selectedSeverityFilter']>) => {
|
|
state.selectedSeverityFilter = action.payload;
|
|
state.currentPage = 1; // Reset to first page when filtering
|
|
},
|
|
|
|
/**
|
|
* Set Category Filter Action
|
|
*
|
|
* Purpose: Set category filter for AI predictions
|
|
*/
|
|
setCategoryFilter: (state, action: PayloadAction<AIPredictionState['selectedCategoryFilter']>) => {
|
|
state.selectedCategoryFilter = action.payload;
|
|
state.currentPage = 1; // Reset to first page when filtering
|
|
},
|
|
|
|
/**
|
|
* Set Sort Action
|
|
*
|
|
* Purpose: Set sort options for AI predictions
|
|
*/
|
|
setSort: (state, action: PayloadAction<{ by: 'date' | 'urgency' | 'confidence' | 'severity'; 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 Case Action
|
|
*
|
|
* Purpose: Set the currently selected AI prediction case
|
|
*/
|
|
setCurrentCase: (state, action: PayloadAction<AIPredictionCase | null>) => {
|
|
state.currentCase = action.payload;
|
|
},
|
|
|
|
/**
|
|
* Update Case in List Action
|
|
*
|
|
* Purpose: Update an AI prediction case in the list
|
|
*/
|
|
updateCaseInList: (state, action: PayloadAction<AIPredictionCase>) => {
|
|
const index = state.predictionCases.findIndex(case_ => case_.patid === action.payload.patid);
|
|
if (index !== -1) {
|
|
state.predictionCases[index] = action.payload;
|
|
}
|
|
|
|
// Update current case if it's the same case
|
|
if (state.currentCase && state.currentCase.patid === action.payload.patid) {
|
|
state.currentCase = action.payload;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Toggle Show Filters Action
|
|
*
|
|
* Purpose: Toggle the display of filter options
|
|
*/
|
|
toggleShowFilters: (state) => {
|
|
state.showFilters = !state.showFilters;
|
|
},
|
|
|
|
/**
|
|
* Clear All Filters Action
|
|
*
|
|
* Purpose: Reset all filters to default values
|
|
*/
|
|
clearAllFilters: (state) => {
|
|
state.searchQuery = '';
|
|
state.selectedUrgencyFilter = 'all';
|
|
state.selectedSeverityFilter = 'all';
|
|
state.selectedCategoryFilter = 'all';
|
|
state.currentPage = 1;
|
|
},
|
|
|
|
/**
|
|
* Select Case Action
|
|
*
|
|
* Purpose: Add/remove case from selected cases
|
|
*/
|
|
toggleCaseSelection: (state, action: PayloadAction<string>) => {
|
|
const caseId = action.payload;
|
|
const index = state.selectedCaseIds.indexOf(caseId);
|
|
|
|
if (index === -1) {
|
|
state.selectedCaseIds.push(caseId);
|
|
} else {
|
|
state.selectedCaseIds.splice(index, 1);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Clear Selected Cases Action
|
|
*
|
|
* Purpose: Clear all selected cases
|
|
*/
|
|
clearSelectedCases: (state) => {
|
|
state.selectedCaseIds = [];
|
|
},
|
|
|
|
/**
|
|
* Clear Cache Action
|
|
*
|
|
* Purpose: Clear AI prediction data cache
|
|
*/
|
|
clearCache: (state) => {
|
|
state.predictionCases = [];
|
|
state.currentCase = null;
|
|
state.lastUpdated = null;
|
|
state.cacheExpiry = null;
|
|
},
|
|
},
|
|
extraReducers: (builder) => {
|
|
// Fetch AI Predictions
|
|
builder
|
|
.addCase(fetchAIPredictions.pending, (state) => {
|
|
state.isLoading = true;
|
|
state.error = null;
|
|
})
|
|
.addCase(fetchAIPredictions.fulfilled, (state, action) => {
|
|
state.isLoading = false;
|
|
state.predictionCases = action.payload.cases;
|
|
state.totalItems = action.payload.total;
|
|
state.lastUpdated = new Date().toLocaleString();
|
|
state.cacheExpiry = new Date(Date.now() + 5 * 60 * 1000).toLocaleString(); // 5 minutes
|
|
state.error = null;
|
|
})
|
|
.addCase(fetchAIPredictions.rejected, (state, action) => {
|
|
state.isLoading = false;
|
|
state.error = action.payload as string;
|
|
});
|
|
|
|
// Fetch AI Prediction Details
|
|
builder
|
|
.addCase(fetchAIPredictionDetails.pending, (state) => {
|
|
state.isLoadingCaseDetails = true;
|
|
state.error = null;
|
|
})
|
|
.addCase(fetchAIPredictionDetails.fulfilled, (state, action) => {
|
|
state.isLoadingCaseDetails = false;
|
|
state.currentCase = action.payload;
|
|
state.error = null;
|
|
})
|
|
.addCase(fetchAIPredictionDetails.rejected, (state, action) => {
|
|
state.isLoadingCaseDetails = false;
|
|
state.error = action.payload as string;
|
|
});
|
|
|
|
// Update Case Review
|
|
builder
|
|
.addCase(updateCaseReview.fulfilled, (state, action) => {
|
|
// Update case in list
|
|
const index = state.predictionCases.findIndex(case_ => case_.patid === action.payload.caseId);
|
|
if (index !== -1) {
|
|
state.predictionCases[index] = {
|
|
...state.predictionCases[index],
|
|
review_status: action.payload.review_status,
|
|
reviewed_by: action.payload.reviewed_by,
|
|
priority: action.payload.priority,
|
|
updated_at: action.payload.updated_at
|
|
};
|
|
}
|
|
|
|
// Update current case if it's the same case
|
|
if (state.currentCase && state.currentCase.patid === action.payload.caseId) {
|
|
state.currentCase = {
|
|
...state.currentCase,
|
|
review_status: action.payload.review_status,
|
|
reviewed_by: action.payload.reviewed_by,
|
|
priority: action.payload.priority,
|
|
updated_at: action.payload.updated_at
|
|
};
|
|
}
|
|
})
|
|
.addCase(updateCaseReview.rejected, (state, action) => {
|
|
state.error = action.payload as string;
|
|
});
|
|
},
|
|
});
|
|
|
|
// ============================================================================
|
|
// EXPORTS
|
|
// ============================================================================
|
|
|
|
export const {
|
|
clearError,
|
|
setSearchQuery,
|
|
setUrgencyFilter,
|
|
setSeverityFilter,
|
|
setCategoryFilter,
|
|
setSort,
|
|
setCurrentPage,
|
|
setItemsPerPage,
|
|
setCurrentCase,
|
|
updateCaseInList,
|
|
toggleShowFilters,
|
|
clearAllFilters,
|
|
toggleCaseSelection,
|
|
clearSelectedCases,
|
|
clearCache,
|
|
} = aiPredictionSlice.actions;
|
|
|
|
export default aiPredictionSlice.reducer;
|
|
|
|
/*
|
|
* End of File: aiPredictionSlice.ts
|
|
* Design & Developed by Tech4Biz Solutions
|
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
|
*/
|