feat: Validate Deployment ID through API before saving

- Add validateDeploymentId() method in api.ts that checks if ID exists
  in user's deployments list
- Update profile.tsx to validate deployment ID before saving
- Show validation error message if ID is invalid
- Display deployment name alongside ID after validation
- Add loading state during validation
This commit is contained in:
Sergei 2026-01-24 20:50:40 -08:00
parent 9ae23cfef3
commit 51d533f133
2 changed files with 108 additions and 10 deletions

View File

@ -54,15 +54,23 @@ function MenuItem({
export default function ProfileScreen() { export default function ProfileScreen() {
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const [deploymentId, setDeploymentId] = useState<string>(''); const [deploymentId, setDeploymentId] = useState<string>('');
const [deploymentName, setDeploymentName] = useState<string>('');
const [showDeploymentModal, setShowDeploymentModal] = useState(false); const [showDeploymentModal, setShowDeploymentModal] = useState(false);
const [tempDeploymentId, setTempDeploymentId] = useState(''); const [tempDeploymentId, setTempDeploymentId] = useState('');
const [isValidating, setIsValidating] = useState(false);
const [validationError, setValidationError] = useState<string | null>(null);
// Load saved deployment ID // Load saved deployment ID and validate to get name
useEffect(() => { useEffect(() => {
const loadDeploymentId = async () => { const loadDeploymentId = async () => {
const saved = await api.getDeploymentId(); const saved = await api.getDeploymentId();
if (saved) { if (saved) {
setDeploymentId(saved); setDeploymentId(saved);
// Validate to get the deployment name
const result = await api.validateDeploymentId(saved);
if (result.ok && result.data?.valid && result.data.name) {
setDeploymentName(result.data.name);
}
} }
}; };
loadDeploymentId(); loadDeploymentId();
@ -70,19 +78,39 @@ export default function ProfileScreen() {
const openDeploymentModal = useCallback(() => { const openDeploymentModal = useCallback(() => {
setTempDeploymentId(deploymentId); setTempDeploymentId(deploymentId);
setValidationError(null);
setShowDeploymentModal(true); setShowDeploymentModal(true);
}, [deploymentId]); }, [deploymentId]);
const saveDeploymentId = useCallback(async () => { const saveDeploymentId = useCallback(async () => {
const trimmed = tempDeploymentId.trim(); const trimmed = tempDeploymentId.trim();
setValidationError(null);
if (trimmed) { if (trimmed) {
await api.setDeploymentId(trimmed); setIsValidating(true);
setDeploymentId(trimmed); try {
const result = await api.validateDeploymentId(trimmed);
if (result.ok && result.data?.valid) {
await api.setDeploymentId(trimmed);
setDeploymentId(trimmed);
setDeploymentName(result.data.name || '');
setShowDeploymentModal(false);
} else if (result.ok && !result.data?.valid) {
setValidationError('Invalid Deployment ID. Please check and try again.');
} else {
setValidationError(result.error?.message || 'Failed to validate Deployment ID');
}
} catch {
setValidationError('Network error. Please try again.');
} finally {
setIsValidating(false);
}
} else { } else {
await api.clearDeploymentId(); await api.clearDeploymentId();
setDeploymentId(''); setDeploymentId('');
setDeploymentName('');
setShowDeploymentModal(false);
} }
setShowDeploymentModal(false);
}, [tempDeploymentId]); }, [tempDeploymentId]);
const openTerms = () => { const openTerms = () => {
@ -139,7 +167,7 @@ export default function ProfileScreen() {
<MenuItem <MenuItem
icon="server-outline" icon="server-outline"
title="Deployment ID" title="Deployment ID"
subtitle={deploymentId || 'Not set (auto)'} subtitle={deploymentId ? `${deploymentId}${deploymentName ? ` (${deploymentName})` : ''}` : 'Not set (auto)'}
onPress={openDeploymentModal} onPress={openDeploymentModal}
/> />
</View> </View>
@ -189,26 +217,37 @@ export default function ProfileScreen() {
Enter the deployment ID to connect to a specific device. Leave empty for automatic detection. Enter the deployment ID to connect to a specific device. Leave empty for automatic detection.
</Text> </Text>
<TextInput <TextInput
style={styles.modalInput} style={[styles.modalInput, validationError && styles.modalInputError]}
placeholder="e.g., 21" placeholder="e.g., 21"
placeholderTextColor={AppColors.textMuted} placeholderTextColor={AppColors.textMuted}
value={tempDeploymentId} value={tempDeploymentId}
onChangeText={setTempDeploymentId} onChangeText={(text) => {
setTempDeploymentId(text);
setValidationError(null);
}}
keyboardType="numeric" keyboardType="numeric"
autoFocus autoFocus
editable={!isValidating}
/> />
{validationError && (
<Text style={styles.errorText}>{validationError}</Text>
)}
<View style={styles.modalButtons}> <View style={styles.modalButtons}>
<TouchableOpacity <TouchableOpacity
style={styles.modalButtonCancel} style={styles.modalButtonCancel}
onPress={() => setShowDeploymentModal(false)} onPress={() => setShowDeploymentModal(false)}
disabled={isValidating}
> >
<Text style={styles.modalButtonCancelText}>Cancel</Text> <Text style={[styles.modalButtonCancelText, isValidating && styles.disabledText]}>Cancel</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={styles.modalButtonSave} style={[styles.modalButtonSave, isValidating && styles.modalButtonDisabled]}
onPress={saveDeploymentId} onPress={saveDeploymentId}
disabled={isValidating}
> >
<Text style={styles.modalButtonSaveText}>Save</Text> <Text style={styles.modalButtonSaveText}>
{isValidating ? 'Validating...' : 'Save'}
</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
@ -376,6 +415,15 @@ const styles = StyleSheet.create({
borderColor: AppColors.border, borderColor: AppColors.border,
marginBottom: Spacing.md, marginBottom: Spacing.md,
}, },
modalInputError: {
borderColor: AppColors.error,
marginBottom: Spacing.xs,
},
errorText: {
color: AppColors.error,
fontSize: FontSizes.sm,
marginBottom: Spacing.md,
},
modalButtons: { modalButtons: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'flex-end', justifyContent: 'flex-end',
@ -400,4 +448,10 @@ const styles = StyleSheet.create({
fontWeight: '600', fontWeight: '600',
color: AppColors.white, color: AppColors.white,
}, },
modalButtonDisabled: {
backgroundColor: AppColors.textMuted,
},
disabledText: {
opacity: 0.5,
},
}); });

View File

@ -215,6 +215,50 @@ class ApiService {
await SecureStore.deleteItemAsync('deploymentId'); await SecureStore.deleteItemAsync('deploymentId');
} }
async validateDeploymentId(deploymentId: string): Promise<ApiResponse<{ valid: boolean; name?: string }>> {
const token = await this.getToken();
const userName = await this.getUserName();
if (!token || !userName) {
return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } };
}
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 validate deployment ID' } };
}
const deploymentIdNum = parseInt(deploymentId, 10);
const deployment = response.data.result_list.find(item => item.deployment_id === deploymentIdNum);
if (deployment) {
return {
ok: true,
data: {
valid: true,
name: `${deployment.first_name} ${deployment.last_name}`.trim(),
},
};
}
return {
ok: true,
data: { valid: false },
};
}
// Beneficiaries (elderly people being monitored) // Beneficiaries (elderly people being monitored)
async getBeneficiaries(): Promise<ApiResponse<{ beneficiaries: Beneficiary[] }>> { async getBeneficiaries(): Promise<ApiResponse<{ beneficiaries: Beneficiary[] }>> {
const token = await this.getToken(); const token = await this.getToken();