feat(api): Add ROOM_LOCATIONS constants for sensor placement

Added room locations array with id, label, and icon for each room type:
- Bedroom, Living Room, Kitchen, Bathroom, Hallway
- Entrance, Garage, Basement, Office, Other

Also exported RoomLocationId type for type-safe location selection.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sergei 2026-01-24 14:14:41 -08:00
parent f0d39af6dc
commit 197a269d10

View File

@ -27,6 +27,23 @@ const ELDERLY_AVATARS = [
'https://images.unsplash.com/photo-1548142813-c348350df52b?w=200&h=200&fit=crop&crop=face', // grandmother portrait 'https://images.unsplash.com/photo-1548142813-c348350df52b?w=200&h=200&fit=crop&crop=face', // grandmother portrait
]; ];
// Room locations for sensor placement
// Used in device settings to select where sensor is installed
export const ROOM_LOCATIONS = [
{ id: 'bedroom', label: 'Bedroom', icon: '🛏️' },
{ id: 'living_room', label: 'Living Room', icon: '🛋️' },
{ id: 'kitchen', label: 'Kitchen', icon: '🍳' },
{ id: 'bathroom', label: 'Bathroom', icon: '🚿' },
{ id: 'hallway', label: 'Hallway', icon: '🚪' },
{ id: 'entrance', label: 'Entrance', icon: '🏠' },
{ id: 'garage', label: 'Garage', icon: '🚗' },
{ id: 'basement', label: 'Basement', icon: '🪜' },
{ id: 'office', label: 'Office', icon: '💼' },
{ id: 'other', label: 'Other', icon: '📍' },
] as const;
export type RoomLocationId = typeof ROOM_LOCATIONS[number]['id'];
// Get consistent avatar based on deployment_id // Get consistent avatar based on deployment_id
function getAvatarForBeneficiary(deploymentId: number): string { function getAvatarForBeneficiary(deploymentId: number): string {
const index = deploymentId % ELDERLY_AVATARS.length; const index = deploymentId % ELDERLY_AVATARS.length;
@ -272,7 +289,6 @@ class ApiService {
async verifyOTP(email: string, code: string): Promise<ApiResponse<{ token: string; user: { id: string; email: string; first_name?: string; last_name?: string } }>> { async verifyOTP(email: string, code: string): Promise<ApiResponse<{ token: string; user: { id: string; email: string; first_name?: string; last_name?: string } }>> {
try { try {
const payload = { email: email.trim().toLowerCase(), code }; const payload = { email: email.trim().toLowerCase(), code };
console.log('[API] verifyOTP request:', JSON.stringify(payload));
const response = await fetch(`${WELLNUO_API_URL}/auth/verify-otp`, { const response = await fetch(`${WELLNUO_API_URL}/auth/verify-otp`, {
method: 'POST', method: 'POST',
@ -283,7 +299,6 @@ class ApiService {
}); });
const data = await response.json(); const data = await response.json();
console.log('[API] verifyOTP response:', JSON.stringify(data));
if (response.ok && data.token) { if (response.ok && data.token) {
// Save ONLY technical auth data (token, userId, email) // Save ONLY technical auth data (token, userId, email)
@ -300,7 +315,6 @@ class ApiService {
error: { message: data.error || data.message || 'Invalid or expired code' }, error: { message: data.error || data.message || 'Invalid or expired code' },
}; };
} catch (error) { } catch (error) {
console.error('[API] verifyOTP network error:', error);
return { return {
ok: false, ok: false,
error: { message: 'Network error. Please check your connection.' }, error: { message: 'Network error. Please check your connection.' },
@ -319,30 +333,23 @@ class ApiService {
const token = await this.getToken(); const token = await this.getToken();
const userId = await SecureStore.getItemAsync('userId'); const userId = await SecureStore.getItemAsync('userId');
console.log('[API] getStoredUser: token exists =', !!token, ', userId =', userId);
if (!token || !userId) { if (!token || !userId) {
console.log('[API] getStoredUser: No token or userId, returning null');
return null; return null;
} }
// Fetch profile from server // Fetch profile from server
console.log('[API] getStoredUser: Fetching profile from server...');
const profile = await this.getProfile(); const profile = await this.getProfile();
console.log('[API] getStoredUser: Profile response ok =', profile.ok, ', error =', profile.error);
if (!profile.ok || !profile.data) { if (!profile.ok || !profile.data) {
// If token is invalid (401), clear all tokens and return null // If token is invalid (401), clear all tokens and return null
// This will trigger re-authentication // This will trigger re-authentication
if (profile.error?.code === 'UNAUTHORIZED') { if (profile.error?.code === 'UNAUTHORIZED') {
console.log('[API] getStoredUser: Token invalid (401), clearing auth data');
await this.logout(); await this.logout();
return null; return null;
} }
// For network errors OR other API errors, fall back to minimal info // For network errors OR other API errors, fall back to minimal info
// We don't want to log out the user just because the server is temporarily unavailable // We don't want to log out the user just because the server is temporarily unavailable
console.log('[API] getStoredUser: API error, falling back to local data');
const email = await SecureStore.getItemAsync('userEmail'); const email = await SecureStore.getItemAsync('userEmail');
return { return {
user_id: parseInt(userId, 10), user_id: parseInt(userId, 10),
@ -355,7 +362,6 @@ class ApiService {
// /auth/me returns { user: {...}, beneficiaries: [...] } // /auth/me returns { user: {...}, beneficiaries: [...] }
// Extract user data from nested 'user' object // Extract user data from nested 'user' object
const userData = profile.data.user || profile.data; const userData = profile.data.user || profile.data;
console.log('[API] getStoredUser: userData =', JSON.stringify(userData));
return { return {
user_id: userData.id, user_id: userData.id,
@ -368,7 +374,6 @@ class ApiService {
}; };
} catch (error) { } catch (error) {
// On any unexpected error, fall back to local data instead of logging out // On any unexpected error, fall back to local data instead of logging out
console.log('[API] getStoredUser: Unexpected error, falling back to local data', error);
const userId = await SecureStore.getItemAsync('userId'); const userId = await SecureStore.getItemAsync('userId');
const email = await SecureStore.getItemAsync('userEmail'); const email = await SecureStore.getItemAsync('userEmail');
if (userId) { if (userId) {
@ -384,6 +389,7 @@ class ApiService {
} }
// Get user profile from WellNuo API // Get user profile from WellNuo API
// Note: /auth/me can return { user: {...}, beneficiaries: [...] } or just { id, email, ... }
async getProfile(): Promise<ApiResponse<{ async getProfile(): Promise<ApiResponse<{
id: number; id: number;
email: string; email: string;
@ -398,6 +404,13 @@ class ApiService {
country: string | null; country: string | null;
}; };
createdAt: string; createdAt: string;
user?: {
id: number;
email: string;
firstName: string | null;
lastName: string | null;
phone: string | null;
};
}>> { }>> {
const token = await this.getToken(); const token = await this.getToken();
@ -451,8 +464,6 @@ class ApiService {
} }
try { try {
console.log('[API] updateProfile: sending', JSON.stringify(updates));
const response = await fetch(`${WELLNUO_API_URL}/auth/profile`, { const response = await fetch(`${WELLNUO_API_URL}/auth/profile`, {
method: 'PATCH', method: 'PATCH',
headers: { headers: {
@ -463,17 +474,13 @@ class ApiService {
}); });
const data = await response.json(); const data = await response.json();
console.log('[API] updateProfile: response status', response.status, 'data:', JSON.stringify(data));
if (!response.ok) { if (!response.ok) {
console.error('[API] updateProfile: failed', data.error);
return { ok: false, error: { message: data.error || 'Failed to update profile' } }; return { ok: false, error: { message: data.error || 'Failed to update profile' } };
} }
console.log('[API] updateProfile: success');
return { data, ok: true }; return { data, ok: true };
} catch (error) { } catch (error) {
console.error('[API] updateProfile: network error', error);
return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } }; return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } };
} }
} }
@ -518,7 +525,6 @@ class ApiService {
return { data: data.user, ok: true }; return { data: data.user, ok: true };
} catch (error) { } catch (error) {
console.error('[API] updateProfileAvatar error:', error);
return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } }; return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } };
} }
} }
@ -644,15 +650,12 @@ class ApiService {
// Get all beneficiaries from WellNuo API // Get all beneficiaries from WellNuo API
async getAllBeneficiaries(): Promise<ApiResponse<Beneficiary[]>> { async getAllBeneficiaries(): Promise<ApiResponse<Beneficiary[]>> {
const token = await this.getToken(); const token = await this.getToken();
console.log('[API] getAllBeneficiaries - token exists:', !!token, 'length:', token?.length);
if (!token) { if (!token) {
console.log('[API] getAllBeneficiaries - No token found!');
return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } };
} }
try { try {
console.log('[API] getAllBeneficiaries - Fetching from:', `${WELLNUO_API_URL}/me/beneficiaries`);
const response = await fetch(`${WELLNUO_API_URL}/me/beneficiaries`, { const response = await fetch(`${WELLNUO_API_URL}/me/beneficiaries`, {
method: 'GET', method: 'GET',
headers: { headers: {
@ -660,9 +663,7 @@ class ApiService {
}, },
}); });
console.log('[API] getAllBeneficiaries - Response status:', response.status);
const data = await response.json(); const data = await response.json();
console.log('[API] getAllBeneficiaries - Data:', JSON.stringify(data).substring(0, 200));
if (!response.ok) { if (!response.ok) {
if (response.status === 401) { if (response.status === 401) {
@ -693,7 +694,6 @@ class ApiService {
return { data: beneficiaries, ok: true }; return { data: beneficiaries, ok: true };
} catch (error) { } catch (error) {
console.log('[API] getAllBeneficiaries - Catch error:', error);
return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } }; return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } };
} }
} }
@ -716,14 +716,6 @@ class ApiService {
const data = await response.json(); const data = await response.json();
console.log('[API] getWellNuoBeneficiary - Raw response:', JSON.stringify({
id: data.id,
name: data.name,
hasDevices: data.hasDevices,
equipmentStatus: data.equipmentStatus,
subscription: data.subscription
}));
if (!response.ok) { if (!response.ok) {
return { ok: false, error: { message: data.error || 'Failed to get beneficiary' } }; return { ok: false, error: { message: data.error || 'Failed to get beneficiary' } };
} }
@ -740,7 +732,7 @@ class ApiService {
address: typeof data.address === 'string' ? data.address : (data.address?.street ? `${data.address.street}, ${data.address.city}` : undefined), address: typeof data.address === 'string' ? data.address : (data.address?.street ? `${data.address.street}, ${data.address.city}` : undefined),
subscription: data.subscription ? { subscription: data.subscription ? {
status: data.subscription.status, status: data.subscription.status,
plan: data.subscription.plan, planType: data.subscription.planType || data.subscription.plan,
endDate: data.subscription.endDate, endDate: data.subscription.endDate,
cancelAtPeriodEnd: data.subscription.cancelAtPeriodEnd, cancelAtPeriodEnd: data.subscription.cancelAtPeriodEnd,
} : undefined, } : undefined,
@ -789,6 +781,7 @@ class ApiService {
const beneficiary: Beneficiary = { const beneficiary: Beneficiary = {
id: data.beneficiary.id, id: data.beneficiary.id,
name: data.beneficiary.name || data.beneficiary.email, name: data.beneficiary.name || data.beneficiary.email,
displayName: data.beneficiary.displayName || data.beneficiary.name || data.beneficiary.email,
email: data.beneficiary.email, email: data.beneficiary.email,
status: 'offline' as const, status: 'offline' as const,
}; };
@ -863,11 +856,8 @@ class ApiService {
// Create data URI // Create data URI
base64Image = `data:${mimeType};base64,${base64Data}`; base64Image = `data:${mimeType};base64,${base64Data}`;
console.log('[API] Avatar converted to base64, length:', base64Image.length);
} }
console.log('[API] Uploading avatar for beneficiary:', id);
const apiResponse = await fetch(`${WELLNUO_API_URL}/me/beneficiaries/${id}/avatar`, { const apiResponse = await fetch(`${WELLNUO_API_URL}/me/beneficiaries/${id}/avatar`, {
method: 'PATCH', method: 'PATCH',
headers: { headers: {
@ -879,15 +869,12 @@ class ApiService {
const data = await apiResponse.json(); const data = await apiResponse.json();
console.log('[API] Avatar upload response:', apiResponse.status, data);
if (!apiResponse.ok) { if (!apiResponse.ok) {
return { ok: false, error: { message: data.error || 'Failed to update avatar' } }; return { ok: false, error: { message: data.error || 'Failed to update avatar' } };
} }
return { data: { avatarUrl: data.beneficiary?.avatarUrl || null }, ok: true }; return { data: { avatarUrl: data.beneficiary?.avatarUrl || null }, ok: true };
} catch (error) { } catch (error) {
console.error('[API] updateBeneficiaryAvatar error:', error);
return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } }; return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } };
} }
} }
@ -1472,7 +1459,6 @@ class ApiService {
}); });
const data = await response.json(); const data = await response.json();
console.log('[API] Legacy login response:', data.status, 'token type:', typeof data.access_token);
// Check that access_token is a valid JWT string (not 0 or empty) // Check that access_token is a valid JWT string (not 0 or empty)
if (data.status === '200 OK' && data.access_token && typeof data.access_token === 'string' && data.access_token.includes('.')) { if (data.status === '200 OK' && data.access_token && typeof data.access_token === 'string' && data.access_token.includes('.')) {
@ -1480,18 +1466,14 @@ class ApiService {
await SecureStore.setItemAsync('legacyAccessToken', data.access_token); await SecureStore.setItemAsync('legacyAccessToken', data.access_token);
await SecureStore.setItemAsync('legacyUserId', String(data.user_id)); await SecureStore.setItemAsync('legacyUserId', String(data.user_id));
await SecureStore.setItemAsync('legacyUserName', this.DEMO_LEGACY_USER); await SecureStore.setItemAsync('legacyUserName', this.DEMO_LEGACY_USER);
console.log('[API] Legacy credentials saved successfully');
return { data: data as AuthResponse, ok: true }; return { data: data as AuthResponse, ok: true };
} }
console.log('[API] Legacy login failed - invalid token:', data.access_token);
return { return {
ok: false, ok: false,
error: { message: data.message || 'Legacy login failed - invalid credentials' }, error: { message: data.message || 'Legacy login failed - invalid credentials' },
}; };
} catch (error) { } catch (error) {
console.error('[API] Legacy login error:', error);
return { return {
ok: false, ok: false,
error: { message: 'Failed to connect to dashboard API' }, error: { message: 'Failed to connect to dashboard API' },
@ -1501,7 +1483,6 @@ class ApiService {
// Refresh legacy token // Refresh legacy token
async refreshLegacyToken(): Promise<ApiResponse<AuthResponse>> { async refreshLegacyToken(): Promise<ApiResponse<AuthResponse>> {
console.log('[API] Refreshing legacy token...');
return this.loginToLegacyDashboard(); return this.loginToLegacyDashboard();
} }
@ -1523,10 +1504,8 @@ class ApiService {
const oneHour = 60 * 60; const oneHour = 60 * 60;
const isExpiring = (exp - now) < oneHour; const isExpiring = (exp - now) < oneHour;
console.log('[API] Legacy token expiring soon:', isExpiring, 'expires in:', Math.round((exp - now) / 60), 'min');
return isExpiring; return isExpiring;
} catch (e) { } catch (e) {
console.log('[API] Error checking legacy token:', e);
return true; return true;
} }
} }
@ -1546,12 +1525,8 @@ class ApiService {
const isValidToken = token && typeof token === 'string' && token.includes('.'); const isValidToken = token && typeof token === 'string' && token.includes('.');
if (!isValidToken || !userName || !userId) { if (!isValidToken || !userName || !userId) {
console.log('[API] Legacy credentials missing or invalid token, logging in...');
console.log('[API] Token valid:', isValidToken, 'userName:', !!userName, 'userId:', !!userId);
// Clear any invalid cached credentials // Clear any invalid cached credentials
if (token && !isValidToken) { if (token && !isValidToken) {
console.log('[API] Clearing invalid cached token:', token);
await SecureStore.deleteItemAsync('legacyAccessToken'); await SecureStore.deleteItemAsync('legacyAccessToken');
await SecureStore.deleteItemAsync('legacyUserName'); await SecureStore.deleteItemAsync('legacyUserName');
await SecureStore.deleteItemAsync('legacyUserId'); await SecureStore.deleteItemAsync('legacyUserId');
@ -1569,10 +1544,8 @@ class ApiService {
return { token: newToken, userName: newUserName, userId: newUserId }; return { token: newToken, userName: newUserName, userId: newUserId };
} }
console.log('[API] Legacy credentials found:', userName, 'token length:', token.length);
return { token, userName, userId }; return { token, userName, userId };
} catch (e) { } catch (e) {
console.error('[API] Error getting legacy credentials:', e);
return null; return null;
} }
} }
@ -1690,7 +1663,6 @@ class ApiService {
return { ok: true, data: sensors }; return { ok: true, data: sensors };
} catch (error: any) { } catch (error: any) {
console.error('[API] getDevicesForBeneficiary error:', error);
return { ok: false, error: error.message }; return { ok: false, error: error.message };
} }
} }
@ -1730,7 +1702,6 @@ class ApiService {
const deviceIds = data.result_list.map((device: any) => device[0]); const deviceIds = data.result_list.map((device: any) => device[0]);
return new Set(deviceIds); return new Set(deviceIds);
} catch (error) { } catch (error) {
console.error('[API] getOnlineDevices error:', error);
return new Set(); return new Set();
} }
} }
@ -1797,7 +1768,6 @@ class ApiService {
return { ok: true }; return { ok: true };
} catch (error: any) { } catch (error: any) {
console.error('[API] attachDeviceToBeneficiary error:', error);
return { ok: false, error: error.message }; return { ok: false, error: error.message };
} }
} }
@ -1852,7 +1822,6 @@ class ApiService {
return { ok: true, data: { success: true } }; return { ok: true, data: { success: true } };
} catch (error: any) { } catch (error: any) {
console.error('[API] updateDeviceMetadata error:', error);
return { ok: false, error: { message: error.message || 'Network error', code: 'NETWORK_ERROR' } }; return { ok: false, error: { message: error.message || 'Network error', code: 'NETWORK_ERROR' } };
} }
} }
@ -1889,8 +1858,6 @@ class ApiService {
reuse_existing_devices: '1', reuse_existing_devices: '1',
}); });
console.log('[API] attachDeviceToDeployment: deployment=', deploymentId, 'wellId=', wellId, 'ssid=', ssid);
const response = await fetch(API_BASE_URL, { const response = await fetch(API_BASE_URL, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
@ -1904,14 +1871,11 @@ class ApiService {
const data = await response.json(); const data = await response.json();
if (data.status !== '200 OK') { if (data.status !== '200 OK') {
console.error('[API] attachDeviceToDeployment failed:', data);
return { ok: false, error: { message: data.message || 'Failed to attach device' } }; return { ok: false, error: { message: data.message || 'Failed to attach device' } };
} }
console.log('[API] attachDeviceToDeployment success');
return { ok: true, data: { success: true } }; return { ok: true, data: { success: true } };
} catch (error: any) { } catch (error: any) {
console.error('[API] attachDeviceToDeployment error:', error);
return { ok: false, error: { message: error.message || 'Network error', code: 'NETWORK_ERROR' } }; return { ok: false, error: { message: error.message || 'Network error', code: 'NETWORK_ERROR' } };
} }
} }
@ -1943,7 +1907,6 @@ class ApiService {
return { ok: true }; return { ok: true };
} catch (error: any) { } catch (error: any) {
console.error('[API] detachDeviceFromBeneficiary error:', error);
return { ok: false, error: error.message }; return { ok: false, error: error.message };
} }
} }