From 19d24e7b00be465c4c68d7cce483edb40cf180ae Mon Sep 17 00:00:00 2001 From: Sergei Date: Fri, 19 Dec 2025 12:57:48 -0800 Subject: [PATCH] Rename patient to beneficiary throughout the app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace all 'patient' terminology with 'beneficiary' - Add Voice AI screen (voice.tsx) with voice_ask API integration - Optimize getAllBeneficiaries() to use single deployments_list API call - Rename PatientDashboardData to BeneficiaryDashboardData - Update UI components: BeneficiaryCard, beneficiary picker modal - Update all error messages and comments 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/(tabs)/beneficiaries/[id]/dashboard.tsx | 4 +- app/(tabs)/chat.tsx | 4 +- app/(tabs)/index.tsx | 78 +-- app/(tabs)/voice.tsx | 661 ++++++++++++++++++++ constants/theme.ts | 2 +- services/api.ts | 87 ++- types/index.ts | 4 +- 7 files changed, 744 insertions(+), 96 deletions(-) create mode 100644 app/(tabs)/voice.tsx diff --git a/app/(tabs)/beneficiaries/[id]/dashboard.tsx b/app/(tabs)/beneficiaries/[id]/dashboard.tsx index 5e1c283..b2d3644 100644 --- a/app/(tabs)/beneficiaries/[id]/dashboard.tsx +++ b/app/(tabs)/beneficiaries/[id]/dashboard.tsx @@ -9,7 +9,7 @@ import { useBeneficiary } from '@/contexts/BeneficiaryContext'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; import { FullScreenError } from '@/components/ui/ErrorMessage'; -// Dashboard URL with patient ID +// Dashboard URL with beneficiary ID (deployment_id) const getDashboardUrl = (deploymentId: string) => `https://react.eluxnetworks.net/dashboard/${deploymentId}`; @@ -25,7 +25,7 @@ export default function BeneficiaryDashboardScreen() { const [userId, setUserId] = useState(null); const [isTokenLoaded, setIsTokenLoaded] = useState(false); - // Build dashboard URL with patient ID + // Build dashboard URL with beneficiary ID const dashboardUrl = id ? getDashboardUrl(id) : 'https://react.eluxnetworks.net/dashboard'; const beneficiaryName = currentBeneficiary?.name || 'Dashboard'; diff --git a/app/(tabs)/chat.tsx b/app/(tabs)/chat.tsx index f9250a2..9244e88 100644 --- a/app/(tabs)/chat.tsx +++ b/app/(tabs)/chat.tsx @@ -47,8 +47,8 @@ export default function ChatScreen() { // Security: require beneficiary to be selected if (!currentBeneficiary?.id) { Alert.alert( - 'Select Patient', - 'Please select a patient from the Patients tab before starting a conversation.', + 'Select Beneficiary', + 'Please select a beneficiary from the Dashboard tab before starting a conversation.', [{ text: 'OK' }] ); return; diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 9c42f56..d88d63a 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -18,16 +18,16 @@ import { api } from '@/services/api'; import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; import type { Beneficiary } from '@/types'; -// Patient card component -interface PatientCardProps { - patient: Beneficiary; +// Beneficiary card component +interface BeneficiaryCardProps { + beneficiary: Beneficiary; onPress: () => void; } -function PatientCard({ patient, onPress }: PatientCardProps) { - const wellnessColor = patient.wellness_score && patient.wellness_score >= 70 +function BeneficiaryCard({ beneficiary, onPress }: BeneficiaryCardProps) { + const wellnessColor = beneficiary.wellness_score && beneficiary.wellness_score >= 70 ? AppColors.success - : patient.wellness_score && patient.wellness_score >= 40 + : beneficiary.wellness_score && beneficiary.wellness_score >= 40 ? '#F59E0B' : AppColors.error; @@ -36,12 +36,12 @@ function PatientCard({ patient, onPress }: PatientCardProps) { {/* Avatar */} - {patient.avatar ? ( - + {beneficiary.avatar ? ( + ) : ( - {patient.name.charAt(0).toUpperCase()} + {beneficiary.name.charAt(0).toUpperCase()} )} @@ -49,23 +49,23 @@ function PatientCard({ patient, onPress }: PatientCardProps) { {/* Info */} - {patient.name} - {patient.last_location && ( + {beneficiary.name} + {beneficiary.last_location && ( - {patient.last_location} + {beneficiary.last_location} )} - {patient.last_activity && ( - {patient.last_activity} + {beneficiary.last_activity && ( + {beneficiary.last_activity} )} {/* Wellness Score */} - {patient.wellness_score !== undefined && ( + {beneficiary.wellness_score !== undefined && ( - {patient.wellness_score}% + {beneficiary.wellness_score}% Wellness @@ -83,31 +83,31 @@ export default function HomeScreen() { const { currentBeneficiary, setCurrentBeneficiary } = useBeneficiary(); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); - const [patients, setPatients] = useState([]); + const [beneficiaries, setBeneficiaries] = useState([]); const [error, setError] = useState(null); - // Load patients from API + // Load beneficiaries from API useEffect(() => { - loadPatients(); + loadBeneficiaries(); }, []); - const loadPatients = async () => { + const loadBeneficiaries = async () => { setIsLoading(true); setError(null); try { - const response = await api.getAllPatients(); + const response = await api.getAllBeneficiaries(); if (response.ok && response.data) { - setPatients(response.data); + setBeneficiaries(response.data); // Auto-select first beneficiary if none selected if (!currentBeneficiary && response.data.length > 0) { setCurrentBeneficiary(response.data[0]); } } else { - setError(response.error?.message || 'Failed to load patients'); + setError(response.error?.message || 'Failed to load beneficiaries'); } } catch (err) { - console.error('Failed to load patients:', err); - setError('Failed to load patients'); + console.error('Failed to load beneficiaries:', err); + setError('Failed to load beneficiaries'); } finally { setIsLoading(false); } @@ -115,15 +115,15 @@ export default function HomeScreen() { const handleRefresh = async () => { setIsRefreshing(true); - await loadPatients(); + await loadBeneficiaries(); setIsRefreshing(false); }; - const handlePatientPress = (patient: Beneficiary) => { + const handleBeneficiaryPress = (beneficiary: Beneficiary) => { // Set current beneficiary in context - setCurrentBeneficiary(patient); - // Navigate to patient dashboard with deployment_id - router.push(`/(tabs)/beneficiaries/${patient.id}/dashboard`); + setCurrentBeneficiary(beneficiary); + // Navigate to beneficiary dashboard with deployment_id + router.push(`/(tabs)/beneficiaries/${beneficiary.id}/dashboard`); }; if (isLoading) { @@ -137,7 +137,7 @@ export default function HomeScreen() { - Loading patients... + Loading beneficiaries... ); @@ -156,21 +156,21 @@ export default function HomeScreen() { - {/* Patient List */} - {patients.length === 0 ? ( + {/* Beneficiary List */} + {beneficiaries.length === 0 ? ( - No Patients - You don't have any patients assigned yet. + No Beneficiaries + You don't have any beneficiaries assigned yet. ) : ( item.id.toString()} renderItem={({ item }) => ( - handlePatientPress(item)} + handleBeneficiaryPress(item)} /> )} contentContainerStyle={styles.listContent} diff --git a/app/(tabs)/voice.tsx b/app/(tabs)/voice.tsx new file mode 100644 index 0000000..cf852b1 --- /dev/null +++ b/app/(tabs)/voice.tsx @@ -0,0 +1,661 @@ +import React, { useState, useCallback, useRef, useEffect } from 'react'; +import { + View, + Text, + StyleSheet, + FlatList, + TextInput, + TouchableOpacity, + KeyboardAvoidingView, + Platform, + Alert, + ActivityIndicator, + Modal, + ScrollView, + Animated, +} from 'react-native'; +import { Ionicons, Feather } from '@expo/vector-icons'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import * as SecureStore from 'expo-secure-store'; +import { Audio } from 'expo-av'; +import { useBeneficiary } from '@/contexts/BeneficiaryContext'; +import { api } from '@/services/api'; +import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme'; +import type { Message, Beneficiary } from '@/types'; + +const OLD_API_URL = 'https://eluxnetworks.net/function/well-api/api'; + +interface VoiceAskResponse { + ok: boolean; + response: { + Command: string; + body: string; + name?: string; + reflected?: string; + language?: string; + time?: number; + }; + status: string; +} + +export default function VoiceAIScreen() { + const { currentBeneficiary, setCurrentBeneficiary } = useBeneficiary(); + const [messages, setMessages] = useState([ + { + id: '1', + role: 'assistant', + content: 'Hello! I\'m your voice assistant for monitoring your loved ones. Select a beneficiary and ask me anything about their wellbeing. You can type or tap the microphone to speak.', + timestamp: new Date(), + }, + ]); + const [input, setInput] = useState(''); + const [isSending, setIsSending] = useState(false); + const [isRecording, setIsRecording] = useState(false); + const [showBeneficiaryPicker, setShowBeneficiaryPicker] = useState(false); + const [beneficiaries, setBeneficiaries] = useState([]); + const flatListRef = useRef(null); + const lastSendTimeRef = useRef(0); + const recordingRef = useRef(null); + const pulseAnim = useRef(new Animated.Value(1)).current; + const SEND_COOLDOWN_MS = 1000; + + // Load beneficiaries on mount + useEffect(() => { + loadBeneficiaries(); + setupAudio(); + return () => { + if (recordingRef.current) { + recordingRef.current.stopAndUnloadAsync(); + } + }; + }, []); + + const setupAudio = async () => { + try { + await Audio.requestPermissionsAsync(); + await Audio.setAudioModeAsync({ + allowsRecordingIOS: true, + playsInSilentModeIOS: true, + }); + } catch (error) { + console.log('Audio setup error:', error); + } + }; + + const loadBeneficiaries = async () => { + const response = await api.getAllBeneficiaries(); + if (response.ok && response.data) { + setBeneficiaries(response.data); + } + }; + + // Pulse animation for recording + useEffect(() => { + if (isRecording) { + const pulse = Animated.loop( + Animated.sequence([ + Animated.timing(pulseAnim, { + toValue: 1.3, + duration: 500, + useNativeDriver: true, + }), + Animated.timing(pulseAnim, { + toValue: 1, + duration: 500, + useNativeDriver: true, + }), + ]) + ); + pulse.start(); + return () => pulse.stop(); + } + }, [isRecording]); + + const sendToVoiceAsk = async (question: string): Promise => { + const token = await SecureStore.getItemAsync('accessToken'); + const userName = await SecureStore.getItemAsync('userName'); + + if (!token || !userName) { + throw new Error('Please log in to use voice assistant'); + } + + if (!currentBeneficiary?.id) { + throw new Error('Please select a beneficiary first'); + } + + const response = await fetch(OLD_API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + function: 'voice_ask', + clientId: '001', + user_name: userName, + token: token, + question: question, + deployment_id: currentBeneficiary.id.toString(), + }).toString(), + }); + + const data: VoiceAskResponse = await response.json(); + + if (data.ok && data.response?.body) { + return data.response.body; + } else if (data.status === '401 Unauthorized') { + throw new Error('Session expired. Please log in again.'); + } else { + throw new Error('Could not get response from voice assistant'); + } + }; + + const startRecording = async () => { + try { + const { status } = await Audio.requestPermissionsAsync(); + if (status !== 'granted') { + Alert.alert('Permission Required', 'Please allow microphone access to use voice input.'); + return; + } + + await Audio.setAudioModeAsync({ + allowsRecordingIOS: true, + playsInSilentModeIOS: true, + }); + + const { recording } = await Audio.Recording.createAsync( + Audio.RecordingOptionsPresets.HIGH_QUALITY + ); + recordingRef.current = recording; + setIsRecording(true); + } catch (error) { + console.error('Failed to start recording:', error); + Alert.alert('Error', 'Could not start recording. Please try again.'); + } + }; + + const stopRecording = async () => { + if (!recordingRef.current) return; + + setIsRecording(false); + try { + await recordingRef.current.stopAndUnloadAsync(); + const uri = recordingRef.current.getURI(); + recordingRef.current = null; + + if (uri) { + // For now, we'll show a message about speech-to-text + // In production, this would send to a speech-to-text service + Alert.alert( + 'Voice Recorded', + 'Speech-to-text conversion requires additional integration. For now, please type your question.', + [{ text: 'OK' }] + ); + } + } catch (error) { + console.error('Failed to stop recording:', error); + } + }; + + const handleSend = useCallback(async () => { + const trimmedInput = input.trim(); + if (!trimmedInput || isSending) return; + + // Debounce + const now = Date.now(); + if (now - lastSendTimeRef.current < SEND_COOLDOWN_MS) return; + lastSendTimeRef.current = now; + + // Require beneficiary selection + if (!currentBeneficiary?.id) { + Alert.alert( + 'Select Beneficiary', + 'Please select a beneficiary first to ask questions about their wellbeing.', + [{ text: 'Select', onPress: () => setShowBeneficiaryPicker(true) }, { text: 'Cancel' }] + ); + return; + } + + const userMessage: Message = { + id: Date.now().toString(), + role: 'user', + content: trimmedInput, + timestamp: new Date(), + }; + + setMessages(prev => [...prev, userMessage]); + setInput(''); + setIsSending(true); + + try { + const aiResponse = await sendToVoiceAsk(trimmedInput); + + const assistantMessage: Message = { + id: (Date.now() + 1).toString(), + role: 'assistant', + content: aiResponse, + timestamp: new Date(), + }; + setMessages(prev => [...prev, assistantMessage]); + } catch (error) { + const errorMessage: Message = { + id: (Date.now() + 1).toString(), + role: 'assistant', + content: `Sorry, I encountered an error: ${error instanceof Error ? error.message : 'Unknown error'}. Please try again.`, + timestamp: new Date(), + }; + setMessages(prev => [...prev, errorMessage]); + } finally { + setIsSending(false); + } + }, [input, isSending, currentBeneficiary, messages]); + + const selectBeneficiary = (beneficiary: Beneficiary) => { + setCurrentBeneficiary(beneficiary); + setShowBeneficiaryPicker(false); + + // Add welcome message for selected beneficiary + const welcomeMessage: Message = { + id: Date.now().toString(), + role: 'assistant', + content: `Great! I'm now ready to answer questions about ${beneficiary.name}. ${beneficiary.wellness_descriptor ? `Current status: ${beneficiary.wellness_descriptor}.` : ''} Ask me anything!`, + timestamp: new Date(), + }; + setMessages(prev => [...prev, welcomeMessage]); + }; + + const renderMessage = ({ item }: { item: Message }) => { + const isUser = item.role === 'user'; + + return ( + + {!isUser && ( + + + + )} + + + {item.content} + + + {item.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + + + + ); + }; + + return ( + + {/* Header */} + + + + + + + Voice AI + + {isSending + ? 'Thinking...' + : currentBeneficiary + ? `Monitoring ${currentBeneficiary.name}` + : 'Select a beneficiary'} + + + + setShowBeneficiaryPicker(true)}> + + + {currentBeneficiary?.name?.split(' ')[0] || 'Select'} + + + + + {/* Messages */} + + item.id} + renderItem={renderMessage} + contentContainerStyle={styles.messagesList} + showsVerticalScrollIndicator={false} + onContentSizeChange={() => flatListRef.current?.scrollToEnd({ animated: true })} + /> + + {/* Recording indicator */} + {isRecording && ( + + + Listening... + + )} + + {/* Input */} + + + + + + + {isSending ? ( + + ) : ( + + )} + + + + + {/* Beneficiary Picker Modal */} + + + + + Select Beneficiary + setShowBeneficiaryPicker(false)}> + + + + + {beneficiaries.length === 0 ? ( + + + Loading beneficiaries... + + ) : ( + beneficiaries.map(beneficiary => ( + selectBeneficiary(beneficiary)} + > + + {beneficiary.name} + + {beneficiary.wellness_descriptor || beneficiary.last_location || 'No data'} + + + + + )) + )} + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: AppColors.surface, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: Spacing.md, + paddingVertical: Spacing.sm, + backgroundColor: AppColors.background, + borderBottomWidth: 1, + borderBottomColor: AppColors.border, + }, + headerInfo: { + flexDirection: 'row', + alignItems: 'center', + }, + headerAvatar: { + width: 40, + height: 40, + borderRadius: BorderRadius.full, + backgroundColor: '#9B59B6', // Purple for Voice AI + justifyContent: 'center', + alignItems: 'center', + marginRight: Spacing.sm, + }, + headerTitle: { + fontSize: FontSizes.lg, + fontWeight: '600', + color: AppColors.textPrimary, + }, + headerSubtitle: { + fontSize: FontSizes.sm, + color: AppColors.success, + }, + beneficiaryButton: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: Spacing.sm, + paddingVertical: Spacing.xs, + backgroundColor: AppColors.surface, + borderRadius: BorderRadius.lg, + borderWidth: 1, + borderColor: AppColors.border, + }, + beneficiaryButtonText: { + marginLeft: Spacing.xs, + fontSize: FontSizes.sm, + color: AppColors.primary, + fontWeight: '500', + }, + chatContainer: { + flex: 1, + }, + messagesList: { + padding: Spacing.md, + paddingBottom: Spacing.lg, + }, + messageContainer: { + flexDirection: 'row', + marginBottom: Spacing.md, + alignItems: 'flex-end', + }, + userMessageContainer: { + justifyContent: 'flex-end', + }, + assistantMessageContainer: { + justifyContent: 'flex-start', + }, + avatarContainer: { + width: 32, + height: 32, + borderRadius: BorderRadius.full, + backgroundColor: '#9B59B6', + justifyContent: 'center', + alignItems: 'center', + marginRight: Spacing.xs, + }, + messageBubble: { + maxWidth: '75%', + padding: Spacing.sm + 4, + borderRadius: BorderRadius.lg, + }, + userBubble: { + backgroundColor: AppColors.primary, + borderBottomRightRadius: BorderRadius.sm, + }, + assistantBubble: { + backgroundColor: AppColors.background, + borderBottomLeftRadius: BorderRadius.sm, + }, + messageText: { + fontSize: FontSizes.base, + lineHeight: 22, + }, + userMessageText: { + color: AppColors.white, + }, + assistantMessageText: { + color: AppColors.textPrimary, + }, + timestamp: { + fontSize: FontSizes.xs, + color: AppColors.textMuted, + marginTop: Spacing.xs, + alignSelf: 'flex-end', + }, + userTimestamp: { + color: 'rgba(255,255,255,0.7)', + }, + recordingIndicator: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: Spacing.sm, + backgroundColor: 'rgba(155, 89, 182, 0.1)', + }, + recordingDot: { + width: 12, + height: 12, + borderRadius: 6, + backgroundColor: '#E74C3C', + marginRight: Spacing.sm, + }, + recordingText: { + fontSize: FontSizes.sm, + color: '#9B59B6', + fontWeight: '500', + }, + inputContainer: { + flexDirection: 'row', + alignItems: 'flex-end', + padding: Spacing.md, + backgroundColor: AppColors.background, + borderTopWidth: 1, + borderTopColor: AppColors.border, + }, + micButton: { + width: 44, + height: 44, + borderRadius: BorderRadius.full, + backgroundColor: AppColors.surface, + justifyContent: 'center', + alignItems: 'center', + marginRight: Spacing.sm, + borderWidth: 1, + borderColor: AppColors.primary, + }, + micButtonActive: { + backgroundColor: '#E74C3C', + borderColor: '#E74C3C', + }, + input: { + flex: 1, + backgroundColor: AppColors.surface, + borderRadius: BorderRadius.xl, + paddingHorizontal: Spacing.md, + paddingVertical: Spacing.sm, + fontSize: FontSizes.base, + color: AppColors.textPrimary, + maxHeight: 100, + marginRight: Spacing.sm, + }, + sendButton: { + width: 44, + height: 44, + borderRadius: BorderRadius.full, + backgroundColor: '#9B59B6', + justifyContent: 'center', + alignItems: 'center', + }, + sendButtonDisabled: { + backgroundColor: AppColors.surface, + }, + // Modal styles + modalOverlay: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.5)', + justifyContent: 'flex-end', + }, + modalContent: { + backgroundColor: AppColors.background, + borderTopLeftRadius: BorderRadius.xl, + borderTopRightRadius: BorderRadius.xl, + maxHeight: '70%', + }, + modalHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + padding: Spacing.md, + borderBottomWidth: 1, + borderBottomColor: AppColors.border, + }, + modalTitle: { + fontSize: FontSizes.lg, + fontWeight: '600', + color: AppColors.textPrimary, + }, + beneficiaryList: { + padding: Spacing.md, + }, + beneficiaryItem: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + padding: Spacing.md, + backgroundColor: AppColors.surface, + borderRadius: BorderRadius.lg, + marginBottom: Spacing.sm, + }, + beneficiaryItemSelected: { + backgroundColor: '#E8F0FE', + borderWidth: 2, + borderColor: AppColors.primary, + }, + beneficiaryInfo: { + flex: 1, + }, + beneficiaryName: { + fontSize: FontSizes.base, + fontWeight: '600', + color: AppColors.textPrimary, + }, + beneficiaryStatus: { + fontSize: FontSizes.sm, + color: AppColors.textMuted, + marginTop: 2, + }, + statusDot: { + width: 10, + height: 10, + borderRadius: 5, + marginLeft: Spacing.sm, + }, + emptyState: { + alignItems: 'center', + padding: Spacing.xl, + }, + emptyStateText: { + marginTop: Spacing.md, + fontSize: FontSizes.base, + color: AppColors.textMuted, + }, +}); diff --git a/constants/theme.ts b/constants/theme.ts index 1c0e1d3..84fbd4f 100644 --- a/constants/theme.ts +++ b/constants/theme.ts @@ -29,7 +29,7 @@ export const AppColors = { textMuted: '#999999', textLight: '#FFFFFF', - // Patient Status + // Beneficiary Status online: '#5AC8A8', offline: '#999999', }; diff --git a/services/api.ts b/services/api.ts index 8de35ec..83000eb 100644 --- a/services/api.ts +++ b/services/api.ts @@ -1,6 +1,6 @@ import * as SecureStore from 'expo-secure-store'; import * as Crypto from 'expo-crypto'; -import type { AuthResponse, ChatResponse, Beneficiary, ApiResponse, ApiError, DashboardSingleResponse, PatientDashboardData } from '@/types'; +import type { AuthResponse, ChatResponse, Beneficiary, ApiResponse, ApiError, DashboardSingleResponse, BeneficiaryDashboardData } from '@/types'; // Callback for handling unauthorized responses (401) let onUnauthorizedCallback: (() => void) | null = null; @@ -217,15 +217,15 @@ class ApiService { } async getBeneficiary(id: number): Promise> { - // Use real API data via getPatientDashboard - const response = await this.getPatientDashboard(id.toString()); + // Use real API data via getBeneficiaryDashboard + const response = await this.getBeneficiaryDashboard(id.toString()); if (!response.ok || !response.data) { return { ok: false, error: response.error || { message: 'Beneficiary not found', code: 'NOT_FOUND' } }; } const data = response.data; - // Determine if patient is "online" based on last_detected_time + // Determine if beneficiary is "online" based on last_detected_time const lastDetected = data.last_detected_time ? new Date(data.last_detected_time) : null; const isRecent = lastDetected && (Date.now() - lastDetected.getTime()) < 30 * 60 * 1000; // 30 min @@ -254,8 +254,8 @@ class ApiService { return { data: beneficiary, ok: true }; } - // Get patient dashboard data by deployment_id - async getPatientDashboard(deploymentId: string): Promise> { + // Get beneficiary dashboard data by deployment_id + async getBeneficiaryDashboard(deploymentId: string): Promise> { const token = await this.getToken(); const userName = await this.getUserName(); @@ -280,65 +280,52 @@ class ApiService { return { ok: false, - error: response.error || { message: 'Failed to get patient data' }, + error: response.error || { message: 'Failed to get beneficiary data' }, }; } - // Get all patients from privileges (deployment_ids) - async getAllPatients(): Promise> { + // Get all beneficiaries using deployments_list API (single fast request) + async getAllBeneficiaries(): Promise> { const token = await this.getToken(); - if (!token) { + const userName = await this.getUserName(); + + if (!token || !userName) { return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; } - const privileges = await SecureStore.getItemAsync('privileges'); - if (!privileges) { - return { ok: true, data: [] }; + // Use deployments_list API - single request for all beneficiaries + const response = await this.makeRequest<{ result_list: Array<{ + deployment_id: number; + email: string; + first_name: string; + last_name: string; + }> }>({ + function: 'deployments_list', + user_name: userName, + token: token, + first: '0', + last: '100', + }); + + if (!response.ok || !response.data?.result_list) { + return { ok: false, error: response.error || { message: 'Failed to get beneficiaries' } }; } - const deploymentIds = privileges.split(',').map(id => id.trim()).filter(id => id); - const patients: Beneficiary[] = []; + const beneficiaries: Beneficiary[] = response.data.result_list.map(item => ({ + id: item.deployment_id, + name: `${item.first_name} ${item.last_name}`.trim(), + avatar: getAvatarForBeneficiary(item.deployment_id), + status: 'offline' as const, // Will be updated when dashboard is loaded + email: item.email, + })); - // Fetch data for each deployment_id - for (const deploymentId of deploymentIds) { - const response = await this.getPatientDashboard(deploymentId); - if (response.ok && response.data) { - const data = response.data; - // Determine if patient is "online" based on last_detected_time - const lastDetected = data.last_detected_time ? new Date(data.last_detected_time) : null; - const isRecent = lastDetected && (Date.now() - lastDetected.getTime()) < 30 * 60 * 1000; // 30 min - - const deploymentId = parseInt(data.deployment_id, 10); - patients.push({ - id: deploymentId, - name: data.name, - avatar: getAvatarForBeneficiary(deploymentId), - status: isRecent ? 'online' : 'offline', - address: data.address, - timezone: data.time_zone, - wellness_score: data.wellness_score_percent, - wellness_descriptor: data.wellness_descriptor, - last_location: data.last_location, - temperature: data.temperature, - units: data.units, - sleep_hours: data.sleep_hours, - bedroom_temperature: data.bedroom_temperature, - before_last_location: data.before_last_location, - last_detected_time: data.last_detected_time, - last_activity: data.last_detected_time - ? formatTimeAgo(new Date(data.last_detected_time)) - : undefined, - }); - } - } - - return { data: patients, ok: true }; + return { data: beneficiaries, ok: true }; } // AI Chat - deploymentId is required, no default value for security async sendMessage(question: string, deploymentId: string): Promise> { if (!deploymentId) { - return { ok: false, error: { message: 'Please select a patient first', code: 'NO_PATIENT_SELECTED' } }; + return { ok: false, error: { message: 'Please select a beneficiary first', code: 'NO_BENEFICIARY_SELECTED' } }; } const token = await this.getToken(); const userName = await this.getUserName(); diff --git a/types/index.ts b/types/index.ts index 42d5239..f16c6bd 100644 --- a/types/index.ts +++ b/types/index.ts @@ -45,11 +45,11 @@ export interface Beneficiary { // Dashboard API response export interface DashboardSingleResponse { - result_list: PatientDashboardData[]; + result_list: BeneficiaryDashboardData[]; status: string; } -export interface PatientDashboardData { +export interface BeneficiaryDashboardData { user_id: number; name: string; address: string;