import * as SecureStore from 'expo-secure-store'; import * as Crypto from 'expo-crypto'; import type { AuthResponse, ChatResponse, Beneficiary, ApiResponse, ApiError, DashboardSingleResponse, PatientDashboardData } from '@/types'; // Callback for handling unauthorized responses (401) let onUnauthorizedCallback: (() => void) | null = null; export function setOnUnauthorizedCallback(callback: () => void) { onUnauthorizedCallback = callback; } const API_BASE_URL = 'https://eluxnetworks.net/function/well-api/api'; const CLIENT_ID = 'MA_001'; // Avatar images for elderly beneficiaries - grandmothers (бабушки) const ELDERLY_AVATARS = [ 'https://images.unsplash.com/photo-1566616213894-2d4e1baee5d8?w=200&h=200&fit=crop&crop=face', // grandmother with gray hair 'https://images.unsplash.com/photo-1544027993-37dbfe43562a?w=200&h=200&fit=crop&crop=face', // elderly woman smiling 'https://images.unsplash.com/photo-1491308056676-205b7c9a7dc1?w=200&h=200&fit=crop&crop=face', // senior woman portrait 'https://images.unsplash.com/photo-1580489944761-15a19d654956?w=200&h=200&fit=crop&crop=face', // older woman glasses 'https://images.unsplash.com/photo-1548142813-c348350df52b?w=200&h=200&fit=crop&crop=face', // grandmother portrait ]; // Get consistent avatar based on deployment_id function getAvatarForBeneficiary(deploymentId: number): string { const index = deploymentId % ELDERLY_AVATARS.length; return ELDERLY_AVATARS[index]; } // Helper function to format time ago function formatTimeAgo(date: Date): string { const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); if (diffMins < 1) return 'Just now'; if (diffMins < 60) return `${diffMins} min ago`; if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`; return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`; } class ApiService { private async getToken(): Promise { try { return await SecureStore.getItemAsync('accessToken'); } catch { return null; } } private async getUserName(): Promise { try { return await SecureStore.getItemAsync('userName'); } catch { return null; } } private generateNonce(): string { const randomBytes = Crypto.getRandomBytes(16); return Array.from(randomBytes) .map(b => b.toString(16).padStart(2, '0')) .join(''); } private async makeRequest(params: Record): Promise> { try { const formData = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { formData.append(key, value); }); const response = await fetch(API_BASE_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: formData.toString(), }); const data = await response.json(); // Handle 401 Unauthorized - trigger logout if (response.status === 401 || data.status === '401' || data.error === 'Unauthorized') { if (onUnauthorizedCallback) { onUnauthorizedCallback(); } return { ok: false, error: { message: 'Session expired. Please login again.', code: 'UNAUTHORIZED', status: 401, }, }; } if (data.status === '200 OK' || data.ok === true) { return { data: data as T, ok: true }; } return { ok: false, error: { message: data.message || data.error || 'Request failed', status: response.status, }, }; } catch (error) { const apiError: ApiError = { message: error instanceof Error ? error.message : 'Network error', code: 'NETWORK_ERROR', }; return { ok: false, error: apiError }; } } // Authentication async login(username: string, password: string): Promise> { const response = await this.makeRequest({ function: 'credentials', user_name: username, ps: password, clientId: CLIENT_ID, nonce: this.generateNonce(), }); if (response.ok && response.data) { // Save credentials to SecureStore await SecureStore.setItemAsync('accessToken', response.data.access_token); await SecureStore.setItemAsync('userId', response.data.user_id.toString()); await SecureStore.setItemAsync('userName', username); await SecureStore.setItemAsync('privileges', response.data.privileges); await SecureStore.setItemAsync('maxRole', response.data.max_role.toString()); } return response; } async logout(): Promise { await SecureStore.deleteItemAsync('accessToken'); await SecureStore.deleteItemAsync('userId'); await SecureStore.deleteItemAsync('userName'); await SecureStore.deleteItemAsync('privileges'); await SecureStore.deleteItemAsync('maxRole'); } async isAuthenticated(): Promise { const token = await this.getToken(); return !!token; } // Get stored user info async getStoredUser() { try { const userId = await SecureStore.getItemAsync('userId'); const userName = await SecureStore.getItemAsync('userName'); const privileges = await SecureStore.getItemAsync('privileges'); const maxRole = await SecureStore.getItemAsync('maxRole'); if (!userId || !userName) return null; return { user_id: parseInt(userId, 10), user_name: userName, privileges: privileges || '', max_role: parseInt(maxRole || '0', 10), }; } catch { return null; } } // Beneficiaries (elderly people being monitored) async getBeneficiaries(): Promise> { const token = await this.getToken(); if (!token) { return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; } // Note: Using mock data since API structure is not fully documented // Replace with actual API call when available const mockBeneficiaries: Beneficiary[] = [ { id: 1, name: 'Julia Smith', status: 'online', relationship: 'Mother', last_activity: '2 min ago', sensor_data: { motion_detected: true, last_motion: '2 min ago', door_status: 'closed', temperature: 22, humidity: 45, }, }, { id: 2, name: 'Robert Johnson', status: 'offline', relationship: 'Father', last_activity: '1 hour ago', sensor_data: { motion_detected: false, last_motion: '1 hour ago', door_status: 'closed', temperature: 21, humidity: 50, }, }, ]; return { data: { beneficiaries: mockBeneficiaries }, ok: true }; } async getBeneficiary(id: number): Promise> { // Use real API data via getPatientDashboard const response = await this.getPatientDashboard(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 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); const beneficiary: Beneficiary = { 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: beneficiary, ok: true }; } // Get patient dashboard data by deployment_id async getPatientDashboard(deploymentId: string): Promise> { const token = await this.getToken(); const userName = await this.getUserName(); if (!token || !userName) { return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; } const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD const response = await this.makeRequest({ function: 'dashboard_single', user_name: userName, token: token, deployment_id: deploymentId, date: today, nonce: this.generateNonce(), }); if (response.ok && response.data?.result_list?.[0]) { return { data: response.data.result_list[0], ok: true }; } return { ok: false, error: response.error || { message: 'Failed to get patient data' }, }; } // Get all patients from privileges (deployment_ids) async getAllPatients(): Promise> { const token = await this.getToken(); if (!token) { return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; } const privileges = await SecureStore.getItemAsync('privileges'); if (!privileges) { return { ok: true, data: [] }; } const deploymentIds = privileges.split(',').map(id => id.trim()).filter(id => id); const patients: Beneficiary[] = []; // 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 }; } // 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' } }; } const token = await this.getToken(); const userName = await this.getUserName(); if (!token || !userName) { return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; } return this.makeRequest({ function: 'voice_ask', clientId: CLIENT_ID, user_name: userName, token: token, question: question, deployment_id: deploymentId, }); } } export const api = new ApiService();