feat: initial implementation — all 35 requirements across phases 1-3

Backend (Spring Boot 3.2 / Java 21 / PostgreSQL):
- JWT auth with BCrypt password hashing
- User profile + Mifflin-St Jeor BMR calculator
- Food search + barcode via OpenFoodFacts API with local cache
- Meal CRUD with user data isolation and ownership checks
- AI photo analysis (OpenAI Vision) with confidence intervals
- AI correction feedback loop for personalisation
- Flyway DB migrations + RFC-7807 error responses

Mobile (React Native / TypeScript):
- Full navigation stack (Auth → Tabs → Home stack)
- Design tokens (WCAG 2.2 AA colours, 8px grid, 48px touch targets)
- 10 screens: Login, Register, Home, Search, Camera, AI Result, Edit Meal,
  Daily Details, History, Profile
- Confidence-aware calorie display (kcal ± range)
- Repeat last meal shortcut + macro tracking

Docs:
- docs/PLAN-AND-REQUIREMENTS.md
- docs/traceability.csv (35 requirements, all Implemented)
This commit is contained in:
2026-05-18 21:56:13 +03:00
commit 91cd18aec6
106 changed files with 13886 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
// Generated by GitHub Copilot
import React, { useState } from 'react';
import { View, Text, ScrollView, StyleSheet, Alert } from 'react-native';
import { useNavigation, useRoute } from '@react-navigation/native';
import AISuggestionCard from '../components/AISuggestionCard';
import Button from '../components/Button';
import { AiSuggestion } from '../services/api';
import { Colors } from '../theme/colors';
import { Spacing } from '../theme/spacing';
/**
* AI result screen — shows detected items with confidence scores.
* NEVER auto-saves. User must confirm or edit first. (REQ-AI-002)
* REQ-MOB-004, REQ-INT-001
*/
export default function AIResultScreen() {
const navigation = useNavigation<any>();
const route = useRoute<any>();
const { analysisId, suggestions: initialSuggestions } = route.params as {
analysisId: string;
suggestions: AiSuggestion[];
};
const [suggestions, setSuggestions] = useState<AiSuggestion[]>(initialSuggestions);
const handleGramsChange = (index: number, grams: number) => {
setSuggestions(prev => prev.map((s, i) =>
i === index
? {
...s,
grams,
estimatedCalories: grams * 2,
confidenceLow: Math.max(0, grams * 2 * (1 - (1 - s.confidence) * 0.4)),
confidenceHigh: grams * 2 * (1 + (1 - s.confidence) * 0.4),
}
: s
));
};
const confirmAndNavigate = () => {
// Pass adjusted suggestions to EditMeal for final save
navigation.navigate('EditMeal', { items: suggestions, analysisId });
};
if (suggestions.length === 0) {
return (
<View style={styles.empty}>
<Text style={styles.emptyText}>No food items detected. Try Search instead.</Text>
<Button label="Search Food" onPress={() => navigation.navigate('Search')} />
<Button label="Retake Photo" variant="secondary" onPress={() => navigation.goBack()} />
</View>
);
}
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<AISuggestionCard suggestions={suggestions} onGramsChange={handleGramsChange} />
<View style={styles.actions}>
<Button
label="✅ Confirm Meal"
onPress={confirmAndNavigate}
accessibilityHint="Proceeds to the edit and save screen"
/>
<Button
label="Edit Items"
variant="secondary"
onPress={confirmAndNavigate}
accessibilityHint="Edit portion sizes before saving"
/>
<Button
label="← Retake Photo"
variant="ghost"
onPress={() => navigation.goBack()}
/>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.background },
content: { padding: Spacing.md },
actions: { gap: Spacing.sm },
empty: {
flex: 1, padding: Spacing.lg, justifyContent: 'center', alignItems: 'center',
backgroundColor: Colors.background,
},
emptyText: {
fontSize: 16, color: Colors.gray700, textAlign: 'center', marginBottom: Spacing.lg,
},
});