From 3aee73a7312916db3ed5c8204e90a45c29011514 Mon Sep 17 00:00:00 2001 From: Sergei Date: Wed, 14 Jan 2026 19:03:06 -0800 Subject: [PATCH] Add WP sensor status system with BLE scanning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented three-tier sensor status (online/warning/offline) with visual indicators and BLE scanning for nearby devices. Features: - WPSensor type with status field (online/warning/offline) - Automatic status calculation based on lastSeen time: • Online: < 5 minutes (fresh data) • Warning: 5 min - 1 hour (potential issue) • Offline: > 1 hour (definitely problem) - Dual sensor display: Connected (API) + Available Nearby (BLE) - BLE scanning button for discovering nearby WP sensors - Action Sheet for offline sensors with Reconnect/Remove options - Updated summary card: Total/Online/Warning/Offline counts - Visual status indicators: colored dots and labels - Graceful error handling for API unavailability Files changed: - types/index.ts: Added WPSensor interface with status and source fields - services/api.ts: Updated getDevicesForBeneficiary with status calculation - equipment.tsx: Complete UI overhaul with BLE scanning and two-tier sensor list 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/(tabs)/beneficiaries/[id]/equipment.tsx | 546 ++++++++++++++------ services/api.ts | 226 +++++++- types/index.ts | 15 + 3 files changed, 637 insertions(+), 150 deletions(-) diff --git a/app/(tabs)/beneficiaries/[id]/equipment.tsx b/app/(tabs)/beneficiaries/[id]/equipment.tsx index dc407c7..8a453d3 100644 --- a/app/(tabs)/beneficiaries/[id]/equipment.tsx +++ b/app/(tabs)/beneficiaries/[id]/equipment.tsx @@ -8,12 +8,17 @@ import { Alert, ActivityIndicator, RefreshControl, + Platform, + ActionSheetIOS, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { SafeAreaView } from 'react-native-safe-area-context'; import { router, useLocalSearchParams } from 'expo-router'; +import * as Device from 'expo-device'; import { useBeneficiary } from '@/contexts/BeneficiaryContext'; +import { useBLE } from '@/contexts/BLEContext'; import { api } from '@/services/api'; +import type { WPSensor } from '@/types'; import { AppColors, BorderRadius, @@ -24,111 +29,54 @@ import { } from '@/constants/theme'; import { BeneficiaryMenu } from '@/components/ui/BeneficiaryMenu'; -interface Device { - id: string; - name: string; - type: 'motion' | 'door' | 'temperature' | 'hub'; - status: 'online' | 'offline'; - lastSeen?: string; - room?: string; -} - -const deviceTypeConfig = { - motion: { - icon: 'body-outline' as const, - label: 'Motion Sensor', - color: AppColors.primary, - bgColor: AppColors.primaryLighter, - }, - door: { - icon: 'enter-outline' as const, - label: 'Door Sensor', - color: AppColors.info, - bgColor: AppColors.infoLight, - }, - temperature: { - icon: 'thermometer-outline' as const, - label: 'Temperature', - color: AppColors.warning, - bgColor: AppColors.warningLight, - }, - hub: { - icon: 'git-network-outline' as const, - label: 'Hub', - color: AppColors.accent, - bgColor: AppColors.accentLight, - }, +const sensorConfig = { + icon: 'water' as const, + label: 'WP Sensor', + color: AppColors.primary, + bgColor: AppColors.primaryLighter, }; export default function EquipmentScreen() { const { id } = useLocalSearchParams<{ id: string }>(); const { currentBeneficiary } = useBeneficiary(); + const { isBLEAvailable, scanForDevices, stopScan } = useBLE(); - const [devices, setDevices] = useState([]); + // Separate state for API sensors (attached) and BLE sensors (nearby) + const [apiSensors, setApiSensors] = useState([]); + const [bleSensors, setBleSensors] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); + const [isScanning, setIsScanning] = useState(false); const [isDetaching, setIsDetaching] = useState(null); const beneficiaryName = currentBeneficiary?.name || 'this person'; useEffect(() => { - loadDevices(); + loadSensors(); }, [id]); - const loadDevices = async () => { + const loadSensors = async () => { if (!id) return; try { - // For now, mock data - replace with actual API call - // const response = await api.getDevices(id); + setIsLoading(true); - // Mock devices for demonstration - const mockDevices: Device[] = [ - { - id: '1', - name: 'Living Room Motion', - type: 'motion', - status: 'online', - lastSeen: '2 min ago', - room: 'Living Room', - }, - { - id: '2', - name: 'Front Door', - type: 'door', - status: 'online', - lastSeen: '5 min ago', - room: 'Entrance', - }, - { - id: '3', - name: 'Bedroom Motion', - type: 'motion', - status: 'offline', - lastSeen: '2 hours ago', - room: 'Bedroom', - }, - { - id: '4', - name: 'Temperature Monitor', - type: 'temperature', - status: 'online', - lastSeen: '1 min ago', - room: 'Kitchen', - }, - { - id: '5', - name: 'WellNuo Hub', - type: 'hub', - status: 'online', - lastSeen: 'Just now', - }, - ]; + // Get WP sensors from API (attached to beneficiary) + const response = await api.getDevicesForBeneficiary(id); - setDevices(mockDevices); + if (!response.ok) { + // If error is "Not authenticated with Legacy API" or network error, + // just show empty state without Alert + console.warn('[Equipment] Could not load sensors:', response.error); + setApiSensors([]); + return; + } + + setApiSensors(response.data); } catch (error) { - console.error('Failed to load devices:', error); - Alert.alert('Error', 'Failed to load devices'); + console.error('[Equipment] Failed to load sensors:', error); + // Show empty state instead of Alert + setApiSensors([]); } finally { setIsLoading(false); setIsRefreshing(false); @@ -137,33 +85,137 @@ export default function EquipmentScreen() { const handleRefresh = useCallback(() => { setIsRefreshing(true); - loadDevices(); + loadSensors(); }, [id]); - const handleDetachDevice = (device: Device) => { + // BLE Scan for nearby sensors + const handleScanNearby = async () => { + if (isScanning) { + // Stop scan + stopScan(); + setIsScanning(false); + return; + } + + setIsScanning(true); + setBleSensors([]); // Clear previous results + + try { + const devices = await scanForDevices(10000); // 10 second scan + + // Convert BLE devices to WPSensor format + const nearbyWPSensors: WPSensor[] = devices + .filter(d => d.name?.startsWith('WP_')) // Only WP sensors + .map(d => { + // Parse WP__ format + const parts = d.name!.split('_'); + const wellId = parseInt(parts[1], 10) || 0; + const mac = parts[2] || d.id.slice(-6); + + return { + deviceId: d.id, + wellId: wellId, + mac: mac, + name: d.name!, + status: 'offline' as const, // Nearby but not attached + lastSeen: new Date(), + beneficiaryId: id!, + deploymentId: 0, // Not attached yet + source: 'ble' as const, // From BLE scan + }; + }); + + // Filter out sensors that are already in API list + const apiDeviceIds = new Set(apiSensors.map(s => s.mac)); + const uniqueBleSensors = nearbyWPSensors.filter(s => !apiDeviceIds.has(s.mac)); + + setBleSensors(uniqueBleSensors); + } catch (error) { + console.error('[Equipment] BLE scan failed:', error); + Alert.alert('Scan Failed', 'Could not scan for nearby sensors. Make sure Bluetooth is enabled.'); + } finally { + setIsScanning(false); + } + }; + + // Handle sensor click - show action sheet for offline, navigate to settings for online + const handleSensorPress = (sensor: WPSensor) => { + // For offline API sensors - show reconnect options + if (sensor.source === 'api' && sensor.status === 'offline') { + if (Platform.OS === 'ios') { + ActionSheetIOS.showActionSheetWithOptions( + { + title: `${sensor.name} is Offline`, + message: `Last seen: ${formatLastSeen(sensor.lastSeen)}`, + options: ['Cancel', 'Reconnect via Bluetooth', 'Remove from this home'], + destructiveButtonIndex: 2, + cancelButtonIndex: 0, + }, + buttonIndex => { + if (buttonIndex === 1) { + // Reconnect - go to setup-wifi flow + router.push(`/(tabs)/beneficiaries/${id}/setup-wifi?deviceId=${sensor.deviceId}&deviceName=${sensor.name}&wellId=${sensor.wellId}` as any); + } else if (buttonIndex === 2) { + // Remove + handleDetachDevice(sensor); + } + } + ); + } else { + // Android fallback + Alert.alert( + `${sensor.name} is Offline`, + `Last seen: ${formatLastSeen(sensor.lastSeen)}\n\nWhat would you like to do?`, + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Reconnect', + onPress: () => router.push(`/(tabs)/beneficiaries/${id}/setup-wifi?deviceId=${sensor.deviceId}&deviceName=${sensor.name}&wellId=${sensor.wellId}` as any) + }, + { + text: 'Remove', + style: 'destructive', + onPress: () => handleDetachDevice(sensor) + }, + ] + ); + } + } + // For BLE nearby sensors - go directly to setup + else if (sensor.source === 'ble') { + router.push(`/(tabs)/beneficiaries/${id}/setup-wifi?deviceId=${sensor.deviceId}&deviceName=${sensor.name}&wellId=${sensor.wellId}` as any); + } + // For online API sensors - navigate to settings + else { + handleDeviceSettings(sensor); + } + }; + + const handleDetachDevice = (sensor: WPSensor) => { Alert.alert( - 'Detach Device', - `Are you sure you want to detach "${device.name}" from ${beneficiaryName}?\n\nThe device will become available for use with another person.`, + 'Detach Sensor', + `Are you sure you want to detach "${sensor.name}" from ${beneficiaryName}?\n\nThe sensor will become available for use with another person.`, [ { text: 'Cancel', style: 'cancel' }, { text: 'Detach', style: 'destructive', onPress: async () => { - setIsDetaching(device.id); + setIsDetaching(sensor.deviceId); try { - // API call to detach device - // await api.detachDevice(id, device.id); + const response = await api.detachDeviceFromBeneficiary(id!, sensor.deviceId); - // Simulate API delay - await new Promise(resolve => setTimeout(resolve, 1000)); + if (!response.ok) { + throw new Error('Failed to detach sensor'); + } // Remove from local state - setDevices(prev => prev.filter(d => d.id !== device.id)); + setApiSensors(prev => prev.filter(s => s.deviceId !== sensor.deviceId)); - Alert.alert('Success', `${device.name} has been detached.`); + Alert.alert('Success', `${sensor.name} has been detached.`); } catch (error) { - Alert.alert('Error', 'Failed to detach device. Please try again.'); + console.error('[Equipment] Failed to detach sensor:', error); + Alert.alert('Error', 'Failed to detach sensor. Please try again.'); } finally { setIsDetaching(null); } @@ -174,14 +226,14 @@ export default function EquipmentScreen() { }; const handleDetachAll = () => { - if (devices.length === 0) { - Alert.alert('No Devices', 'There are no devices to detach.'); + if (apiSensors.length === 0) { + Alert.alert('No Sensors', 'There are no sensors to detach.'); return; } Alert.alert( - 'Detach All Devices', - `Are you sure you want to detach all ${devices.length} devices from ${beneficiaryName}?\n\nThis action cannot be undone.`, + 'Detach All Sensors', + `Are you sure you want to detach all ${apiSensors.length} sensors from ${beneficiaryName}?\n\nThis action cannot be undone.`, [ { text: 'Cancel', style: 'cancel' }, { @@ -190,16 +242,16 @@ export default function EquipmentScreen() { onPress: async () => { setIsLoading(true); try { - // API call to detach all devices - // await api.detachAllDevices(id); + // Detach all sensors sequentially + for (const sensor of apiSensors) { + await api.detachDeviceFromBeneficiary(id!, sensor.deviceId); + } - // Simulate API delay - await new Promise(resolve => setTimeout(resolve, 1500)); - - setDevices([]); - Alert.alert('Success', 'All devices have been detached.'); + setApiSensors([]); + Alert.alert('Success', 'All sensors have been detached.'); } catch (error) { - Alert.alert('Error', 'Failed to detach devices. Please try again.'); + console.error('[Equipment] Failed to detach all sensors:', error); + Alert.alert('Error', 'Failed to detach sensors. Please try again.'); } finally { setIsLoading(false); } @@ -209,11 +261,56 @@ export default function EquipmentScreen() { ); }; - const handleAddDevice = () => { - router.push({ - pathname: '/(auth)/activate', - params: { lovedOneName: beneficiaryName, beneficiaryId: id }, - }); + const handleAddSensor = () => { + // Navigate to Add Sensor screen + router.push(`/(tabs)/beneficiaries/${id}/add-sensor` as any); + }; + + const handleDeviceSettings = (sensor: WPSensor) => { + // Navigate to Device Settings screen + router.push(`/(tabs)/beneficiaries/${id}/device-settings/${sensor.deviceId}` as any); + }; + + const formatLastSeen = (lastSeen: Date): string => { + const now = new Date(); + const diffMs = now.getTime() - lastSeen.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + 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`; + }; + + const getStatusColor = (status: 'online' | 'warning' | 'offline') => { + switch (status) { + case 'online': + return AppColors.success; + case 'warning': + return AppColors.warning; + case 'offline': + return AppColors.error; + } + }; + + const getStatusLabel = (status: 'online' | 'warning' | 'offline') => { + switch (status) { + case 'online': + return 'Online'; + case 'warning': + return 'Warning'; + case 'offline': + return 'Offline'; + } + }; + + const getSignalStrength = (rssi: number): string => { + if (rssi >= -50) return 'Excellent'; + if (rssi >= -60) return 'Good'; + if (rssi >= -70) return 'Fair'; + return 'Weak'; }; if (isLoading) { @@ -243,7 +340,7 @@ export default function EquipmentScreen() { Sensors - + + {/* Simulator Warning */} + {!isBLEAvailable && ( + + + + Running in Simulator - BLE features use mock data + + + )} + - {devices.length} - Total Devices + {apiSensors.length} + Total - {devices.filter(d => d.status === 'online').length} + {apiSensors.filter(s => s.status === 'online').length} Online + + + {apiSensors.filter(s => s.status === 'warning').length} + + Warning + + - {devices.filter(d => d.status === 'offline').length} + {apiSensors.filter(s => s.status === 'offline').length} Offline - {/* Devices List */} - {devices.length === 0 ? ( + {/* Connected Sensors Section */} + {apiSensors.length === 0 ? ( - + - No Devices Connected + No Sensors Connected - Add sensors to start monitoring {beneficiaryName}'s wellness. + Add WP sensors to start monitoring {beneficiaryName}'s wellness. - + - Add Device + Add Sensor ) : ( <> - Connected Devices + Connected Sensors ({apiSensors.length}) - {devices.map((device) => { - const config = deviceTypeConfig[device.type]; - const isDetachingThis = isDetaching === device.id; + {apiSensors.map((sensor) => { + const isDetachingThis = isDetaching === sensor.deviceId; + const sensorConfig = { + icon: 'water' as const, + color: AppColors.primary, + bgColor: AppColors.primaryLighter, + }; return ( - + handleSensorPress(sensor)} + activeOpacity={0.7} + > - - + + - {device.name} + {sensor.name} - - {device.status === 'online' ? 'Online' : 'Offline'} + + {getStatusLabel(sensor.status)} - {device.room && ( - <> - - {device.room} - - )} + + {formatLastSeen(sensor.lastSeen)} + {sensor.location && ( + {sensor.location} + )} handleDetachDevice(device)} + onPress={(e) => { + e.stopPropagation(); + handleDetachDevice(sensor); + }} disabled={isDetachingThis} > {isDetachingThis ? ( @@ -346,30 +471,93 @@ export default function EquipmentScreen() { )} - + ); })} {/* Detach All Button */} - {devices.length > 1 && ( + {apiSensors.length > 1 && ( - Detach All Devices + Detach All Sensors )} )} + {/* Scan Nearby Button */} + + {isScanning ? ( + <> + + Scanning... ({bleSensors.length} found) + + ) : ( + <> + + + {bleSensors.length > 0 ? 'Scan Again' : 'Scan for Nearby Sensors'} + + + )} + + + {/* Available Nearby Section */} + {bleSensors.length > 0 && ( + <> + Available Nearby ({bleSensors.length}) + + {bleSensors.map((sensor) => { + const sensorConfig = { + icon: 'water-outline' as const, + color: AppColors.textMuted, + bgColor: AppColors.surface, + }; + + return ( + handleSensorPress(sensor)} + activeOpacity={0.7} + > + + + + + + {sensor.name} + + Not Connected + + Tap to connect + + + + + + + ); + })} + + + )} + {/* Info Section */} - About Equipment + About Sensors - Detaching a device will remove it from {beneficiaryName}'s monitoring setup. - You can then attach it to another person or re-attach it later using the activation code. + WP sensors monitor wellness metrics via WiFi. Tap a sensor to configure WiFi settings, view detailed status, or troubleshoot connectivity issues. + {'\n\n'} + Detaching a sensor removes it from {beneficiaryName}'s monitoring setup. You can then attach it to another person or re-attach it later. @@ -619,4 +807,64 @@ const styles = StyleSheet.create({ color: AppColors.info, lineHeight: 20, }, + // WiFi Info + wifiInfo: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + marginTop: 4, + }, + wifiText: { + fontSize: FontSizes.xs, + color: AppColors.textMuted, + }, + // Simulator Warning + simulatorWarning: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: AppColors.warningLight, + paddingVertical: Spacing.xs, + paddingHorizontal: Spacing.md, + gap: Spacing.xs, + borderBottomWidth: 1, + borderBottomColor: AppColors.warning, + }, + simulatorWarningText: { + fontSize: FontSizes.xs, + color: AppColors.warning, + fontWeight: FontWeights.medium, + flex: 1, + }, + // Scan Button + scanButton: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: AppColors.primary, + paddingVertical: Spacing.md, + borderRadius: BorderRadius.lg, + marginTop: Spacing.lg, + marginBottom: Spacing.lg, + gap: Spacing.sm, + ...Shadows.md, + }, + scanButtonActive: { + backgroundColor: AppColors.secondary, + }, + scanButtonText: { + fontSize: FontSizes.base, + fontWeight: FontWeights.semibold, + color: AppColors.white, + }, + // Nearby Device Card + nearbyDeviceCard: { + borderWidth: 1, + borderColor: AppColors.border, + borderStyle: 'dashed', + }, + deviceLocation: { + fontSize: FontSizes.xs, + color: AppColors.textMuted, + marginTop: 2, + }, }); diff --git a/services/api.ts b/services/api.ts index c37e425..c81f8ad 100644 --- a/services/api.ts +++ b/services/api.ts @@ -1,4 +1,4 @@ -import type { ApiError, ApiResponse, AuthResponse, Beneficiary, BeneficiaryDashboardData, ChatResponse, DashboardSingleResponse, NotificationSettings } from '@/types'; +import type { ApiError, ApiResponse, AuthResponse, Beneficiary, BeneficiaryDashboardData, ChatResponse, DashboardSingleResponse, NotificationSettings, WPSensor } from '@/types'; import { File } from 'expo-file-system'; import * as SecureStore from 'expo-secure-store'; import AsyncStorage from '@react-native-async-storage/async-storage'; @@ -1534,6 +1534,230 @@ class ApiService { getDemoDeploymentId(): number { return this.DEMO_DEPLOYMENT_ID; } + + // ============================================================================ + // WP SENSORS / DEVICES MANAGEMENT + // ============================================================================ + + /** + * Get all devices for a beneficiary + * Returns WP sensors with online/offline status + */ + async getDevicesForBeneficiary(beneficiaryId: string) { + try { + // Get beneficiary's deployment_id from PostgreSQL + const response = await fetch(`${this.baseUrl}/me/beneficiaries/${beneficiaryId}`); + if (!response.ok) throw new Error('Failed to get beneficiary'); + + const beneficiary = await response.json(); + const deploymentId = beneficiary.deploymentId; + + if (!deploymentId) { + return { ok: true, data: [] }; // No deployment = no devices + } + + // Get Legacy API credentials + const creds = await this.getLegacyCredentials(); + if (!creds) return { ok: false, error: 'Not authenticated with Legacy API' }; + + // Get devices from Legacy API + const formData = new URLSearchParams({ + function: 'device_list_by_deployment', + user_name: creds.userName, + token: creds.token, + deployment_id: deploymentId.toString(), + first: '0', + last: '100', + }); + + const devicesResponse = await fetch(this.legacyApiUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: formData.toString(), + }); + + if (!devicesResponse.ok) { + throw new Error('Failed to fetch devices from Legacy API'); + } + + const devicesData = await devicesResponse.json(); + + if (!devicesData.result_list || devicesData.result_list.length === 0) { + return { ok: true, data: [] }; + } + + // Get online status + const onlineDevices = await this.getOnlineDevices(deploymentId); + + // Transform to WPSensor format with status calculation + const sensors: WPSensor[] = devicesData.result_list.map((device: any) => { + const [deviceId, wellId, mac, lastSeenTimestamp, location, description] = device; + const lastSeen = new Date(lastSeenTimestamp * 1000); + + // Calculate status based on lastSeen time + const now = new Date(); + const diffMinutes = (now.getTime() - lastSeen.getTime()) / (1000 * 60); + + let status: 'online' | 'warning' | 'offline'; + if (diffMinutes < 5) { + status = 'online'; // 🟢 Fresh data + } else if (diffMinutes < 60) { + status = 'warning'; // 🟡 Might be issue + } else { + status = 'offline'; // 🔴 Definitely problem + } + + return { + deviceId: deviceId.toString(), + wellId: parseInt(wellId, 10), + mac: mac, + name: `WP_${wellId}_${mac.slice(-6).toLowerCase()}`, + status: status, + lastSeen: lastSeen, + location: location || '', + description: description || '', + beneficiaryId: beneficiaryId, + deploymentId: deploymentId, + source: 'api', // From API = attached to beneficiary + }; + }); + + return { ok: true, data: sensors }; + } catch (error: any) { + console.error('[API] getDevicesForBeneficiary error:', error); + return { ok: false, error: error.message }; + } + } + + /** + * Get online devices for a deployment (using fresh=true) + * Returns Set of device_ids that are online + */ + private async getOnlineDevices(deploymentId: number): Promise> { + try { + const creds = await this.getLegacyCredentials(); + if (!creds) return new Set(); + + const formData = new URLSearchParams({ + function: 'request_devices', + user_name: creds.userName, + token: creds.token, + deployment_id: deploymentId.toString(), + group_id: 'All', + location: 'All', + fresh: 'true', // Only online devices + }); + + const response = await fetch(this.legacyApiUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: formData.toString(), + }); + + if (!response.ok) return new Set(); + + const data = await response.json(); + + if (!data.result_list) return new Set(); + + // Extract device_ids from result + const deviceIds = data.result_list.map((device: any) => device[0]); + return new Set(deviceIds); + } catch (error) { + console.error('[API] getOnlineDevices error:', error); + return new Set(); + } + } + + /** + * Attach device to beneficiary's deployment + */ + async attachDeviceToBeneficiary( + beneficiaryId: string, + wellId: number, + ssid: string, + password: string + ) { + try { + // Get beneficiary details + const response = await fetch(`${this.baseUrl}/me/beneficiaries/${beneficiaryId}`); + if (!response.ok) throw new Error('Failed to get beneficiary'); + + const beneficiary = await response.json(); + const deploymentId = beneficiary.deploymentId; + + if (!deploymentId) { + throw new Error('Beneficiary has no deployment'); + } + + const creds = await this.getLegacyCredentials(); + if (!creds) throw new Error('Not authenticated with Legacy API'); + + // Call set_deployment to attach device + const formData = new URLSearchParams({ + function: 'set_deployment', + user_name: creds.userName, + token: creds.token, + deployment: deploymentId.toString(), + devices: JSON.stringify([wellId]), + wifis: JSON.stringify([`${ssid}|${password}`]), + reuse_existing_devices: '1', + }); + + const attachResponse = await fetch(this.legacyApiUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: formData.toString(), + }); + + if (!attachResponse.ok) { + throw new Error('Failed to attach device'); + } + + const data = await attachResponse.json(); + + if (data.status !== '200 OK') { + throw new Error(data.message || 'Failed to attach device'); + } + + return { ok: true }; + } catch (error: any) { + console.error('[API] attachDeviceToBeneficiary error:', error); + return { ok: false, error: error.message }; + } + } + + /** + * Detach device from beneficiary + */ + async detachDeviceFromBeneficiary(beneficiaryId: string, deviceId: string) { + try { + const creds = await this.getLegacyCredentials(); + if (!creds) throw new Error('Not authenticated with Legacy API'); + + // Set device's deployment to 0 (unassigned) + const formData = new URLSearchParams({ + function: 'device_form', + user_name: creds.userName, + token: creds.token, + device_id: deviceId, + deployment_id: '0', // Unassign + }); + + const response = await fetch(this.legacyApiUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: formData.toString(), + }); + + if (!response.ok) throw new Error('Failed to detach device'); + + return { ok: true }; + } catch (error: any) { + console.error('[API] detachDeviceFromBeneficiary error:', error); + return { ok: false, error: error.message }; + } + } } export const api = new ApiService(); diff --git a/types/index.ts b/types/index.ts index 66e494a..03dd7b5 100644 --- a/types/index.ts +++ b/types/index.ts @@ -43,6 +43,21 @@ export interface BeneficiaryDevice { lastSeen?: string; } +// WP Sensor (Water Pressure sensor) from Legacy API +export interface WPSensor { + deviceId: string; // Device ID from Legacy API + wellId: number; // Well ID (physical device identifier) + mac: string; // MAC address + name: string; // Display name (e.g., "WP_12_a1b2c3") + status: 'online' | 'warning' | 'offline'; // Connection status + lastSeen: Date; // Last data transmission time + location?: string; // Physical location + description?: string; // User description + beneficiaryId: string; // Associated beneficiary + deploymentId: number; // Legacy API deployment ID + source: 'api' | 'ble'; // Data source (API = attached, BLE = nearby) +} + // Equipment/Kit delivery status export type EquipmentStatus = | 'none' // No equipment ordered