Changes: - Updated app.json, eas.json configurations - Modified login, chat, profile, dashboard screens - Added profile subpages (about, edit, help, language, notifications, privacy, subscription, support, terms) - Updated BeneficiaryContext - Updated API service and types - Updated discussion questions scheme - Added .history to gitignore 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
292 lines
9.1 KiB
TypeScript
292 lines
9.1 KiB
TypeScript
import * as SecureStore from 'expo-secure-store';
|
|
import type { AuthResponse, ChatResponse, Beneficiary, ApiResponse, ApiError, DashboardSingleResponse, PatientDashboardData } from '@/types';
|
|
|
|
const API_BASE_URL = 'https://eluxnetworks.net/function/well-api/api';
|
|
const CLIENT_ID = 'MA_001';
|
|
|
|
// 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<string | null> {
|
|
try {
|
|
return await SecureStore.getItemAsync('accessToken');
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private async getUserName(): Promise<string | null> {
|
|
try {
|
|
return await SecureStore.getItemAsync('userName');
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private generateNonce(): string {
|
|
return Math.floor(Math.random() * 1000000).toString();
|
|
}
|
|
|
|
private async makeRequest<T>(params: Record<string, string>): Promise<ApiResponse<T>> {
|
|
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();
|
|
|
|
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<ApiResponse<AuthResponse>> {
|
|
const response = await this.makeRequest<AuthResponse>({
|
|
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<void> {
|
|
await SecureStore.deleteItemAsync('accessToken');
|
|
await SecureStore.deleteItemAsync('userId');
|
|
await SecureStore.deleteItemAsync('userName');
|
|
await SecureStore.deleteItemAsync('privileges');
|
|
await SecureStore.deleteItemAsync('maxRole');
|
|
}
|
|
|
|
async isAuthenticated(): Promise<boolean> {
|
|
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<ApiResponse<{ beneficiaries: Beneficiary[] }>> {
|
|
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<ApiResponse<Beneficiary>> {
|
|
const response = await this.getBeneficiaries();
|
|
if (!response.ok || !response.data) {
|
|
return { ok: false, error: response.error };
|
|
}
|
|
|
|
const beneficiary = response.data.beneficiaries.find((b) => b.id === id);
|
|
if (!beneficiary) {
|
|
return { ok: false, error: { message: 'Beneficiary not found', code: 'NOT_FOUND' } };
|
|
}
|
|
|
|
return { data: beneficiary, ok: true };
|
|
}
|
|
|
|
// Get patient dashboard data by deployment_id
|
|
async getPatientDashboard(deploymentId: string): Promise<ApiResponse<PatientDashboardData>> {
|
|
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<DashboardSingleResponse>({
|
|
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<ApiResponse<Beneficiary[]>> {
|
|
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
|
|
|
|
patients.push({
|
|
id: parseInt(data.deployment_id, 10),
|
|
name: data.name,
|
|
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
|
|
async sendMessage(question: string, deploymentId: string = '21'): Promise<ApiResponse<ChatResponse>> {
|
|
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<ChatResponse>({
|
|
function: 'voice_ask',
|
|
clientId: CLIENT_ID,
|
|
user_name: userName,
|
|
token: token,
|
|
question: question,
|
|
deployment_id: deploymentId,
|
|
});
|
|
}
|
|
}
|
|
|
|
export const api = new ApiService();
|