Première grosse version fonctionnel

This commit is contained in:
2026-04-22 23:30:31 +02:00
commit ccbe5ad801
329 changed files with 29518 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
class User {
final int id;
final String name;
final String email;
User({
required this.id,
required this.name,
required this.email,
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
name: json['name'],
email: json['email'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
};
}
}
@@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/user.dart';
import '../services/auth_service.dart';
class AuthProvider with ChangeNotifier {
final AuthService _authService = AuthService();
static const _platform = MethodChannel('com.cesizen/wearable');
User? _user;
bool _isLoading = false;
String? _errorMessage;
AuthProvider() {
// Écouter les demandes de mise à jour venant de la couche native (montre -> téléphone)
_platform.setMethodCallHandler((call) async {
if (call.method == "requestStatusUpdate") {
_syncWithWear();
}
});
// Tenter de restaurer la session au lancement
_checkInitialAuth();
}
Future<void> _checkInitialAuth() async {
_isLoading = true;
notifyListeners();
final result = await _authService.getProfile();
if (result['status'] == 'success') {
_user = result['user'];
_syncWithWear();
}
_isLoading = false;
notifyListeners();
}
User? get user => _user;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
bool get isAuthenticated => _user != null;
void _syncWithWear() {
try {
_platform.invokeMethod('sendAuthStatus', {
'status': isAuthenticated ? 'authenticated' : 'unauthenticated',
'userName': _user?.name ?? '',
});
} catch (e) {
// Échec silencieux si pas d'appareil Wear OS à proximité
debugPrint("Wear OS sync failed: $e");
}
}
Future<bool> login(String email, String password) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
final result = await _authService.login(email, password);
_isLoading = false;
if (result['status'] == 'success') {
_user = result['user'];
_syncWithWear();
notifyListeners();
return true;
} else {
_errorMessage = result['message'];
notifyListeners();
return false;
}
}
Future<bool> register(String name, String email, String password, String passwordConfirmation) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
final result = await _authService.register(name, email, password, passwordConfirmation);
_isLoading = false;
if (result['status'] == 'success') {
_user = result['user'];
_syncWithWear();
notifyListeners();
return true;
} else {
if (result['errors'] is Map) {
_errorMessage = (result['errors'] as Map).values.first[0];
} else {
_errorMessage = result['errors'].toString();
}
notifyListeners();
return false;
}
}
Future<void> logout() async {
await _authService.logout();
_user = null;
_syncWithWear();
notifyListeners();
}
Future<bool> updateProfile(String name, String email, {String? password}) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
final result = await _authService.updateProfile(name, email, password: password);
_isLoading = false;
if (result['status'] == 'success') {
_user = result['user'];
_syncWithWear();
notifyListeners();
return true;
} else {
_errorMessage = result['message'];
notifyListeners();
return false;
}
}
}
@@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/auth_provider.dart';
import 'register_screen.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
void _login() async {
if (_formKey.currentState!.validate()) {
final authProvider = Provider.of<AuthProvider>(context, listen: false);
final success = await authProvider.login(
_emailController.text,
_passwordController.text,
);
if (success) {
// Rediriger vers l'accueil ou le dashboard
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Connexion réussie')),
);
Navigator.of(context).pop(); // Retourne à l'écran précédent (HomeScreen)
}
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(authProvider.errorMessage ?? 'Échec de la connexion')),
);
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Connexion CESIZen')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Bienvenue sur CESIZen',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 32),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) return 'Veuillez entrer votre email';
if (!value.contains('@')) return 'Email invalide';
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Mot de passe',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.lock),
),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) return 'Veuillez entrer votre mot de passe';
return null;
},
),
const SizedBox(height: 24),
Consumer<AuthProvider>(
builder: (context, auth, child) {
return auth.isLoading
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: _login,
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(50),
),
child: const Text('Se connecter'),
);
},
),
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const RegisterScreen()),
);
},
child: const Text("Pas encore de compte ? S'inscrire"),
),
],
),
),
),
);
}
}
@@ -0,0 +1,267 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/auth_provider.dart';
import '../../tracker/providers/emotion_provider.dart';
import 'package:intl/intl.dart';
class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key});
@override
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _nameController;
late TextEditingController _emailController;
late TextEditingController _passwordController;
late String _currentEmail;
bool _isEditing = false;
@override
void initState() {
super.initState();
final user = Provider.of<AuthProvider>(context, listen: false).user;
_currentEmail = user?.email ?? '';
_nameController = TextEditingController(text: user?.name);
_emailController = TextEditingController(text: _currentEmail);
_passwordController = TextEditingController();
_emailController.addListener(() {
if (mounted) setState(() {});
});
// Charger les émotions pour le journal
WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<EmotionProvider>(context, listen: false).loadEmotions();
});
}
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
void _submitUpdate() async {
if (_formKey.currentState!.validate()) {
final authProvider = Provider.of<AuthProvider>(context, listen: false);
final String newName = _nameController.text.trim();
final String newEmail = _emailController.text.trim();
final String? newPassword = _passwordController.text.isNotEmpty ? _passwordController.text : null;
final success = await authProvider.updateProfile(
newName,
newEmail,
password: newPassword,
);
if (mounted) {
if (success) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Profil mis à jour avec succès')),
);
setState(() {
_isEditing = false;
_currentEmail = newEmail;
_passwordController.clear();
});
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(authProvider.errorMessage ?? 'Erreur lors de la mise à jour')),
);
}
}
}
}
@override
Widget build(BuildContext context) {
final authProvider = Provider.of<AuthProvider>(context);
final emotionProvider = Provider.of<EmotionProvider>(context);
final user = authProvider.user;
final bool emailChanged = _emailController.text.trim() != _currentEmail.trim();
return Scaffold(
appBar: AppBar(
title: const Text('Mon Espace Santé'),
backgroundColor: Colors.white,
elevation: 0,
actions: [
IconButton(
icon: Icon(_isEditing ? Icons.close : Icons.edit),
onPressed: () {
setState(() {
_isEditing = !_isEditing;
if (!_isEditing) {
_nameController.text = user?.name ?? '';
_emailController.text = user?.email ?? '';
_passwordController.clear();
}
});
},
)
],
),
body: SingleChildScrollView(
child: Column(
children: [
// Section Profil
Padding(
padding: const EdgeInsets.all(24.0),
child: Form(
key: _formKey,
child: Column(
children: [
const CircleAvatar(
radius: 50,
backgroundColor: Color(0xFF000080),
child: Icon(Icons.person, size: 50, color: Colors.white),
),
const SizedBox(height: 20),
TextFormField(
controller: _nameController,
enabled: _isEditing,
decoration: const InputDecoration(
labelText: 'Nom',
prefixIcon: Icon(Icons.person_outline),
border: OutlineInputBorder(),
),
validator: (value) => (value == null || value.trim().isEmpty) ? 'Veuillez entrer un nom' : null,
),
const SizedBox(height: 15),
TextFormField(
controller: _emailController,
enabled: _isEditing,
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email_outlined),
border: OutlineInputBorder(),
),
validator: (value) {
final email = value?.trim() ?? '';
if (email.isEmpty) return 'Veuillez entrer un email';
if (!email.contains('@')) return 'Email invalide';
return null;
},
),
if (_isEditing) ...[
const SizedBox(height: 15),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: InputDecoration(
labelText: emailChanged ? 'Mot de passe requis' : 'Nouveau mot de passe',
prefixIcon: const Icon(Icons.lock_outline),
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: authProvider.isLoading ? null : _submitUpdate,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF000080),
foregroundColor: Colors.white,
minimumSize: const Size.fromHeight(50),
),
child: const Text('ENREGISTRER'),
),
],
],
),
),
),
// Section Journal Émotionnel (Le "Journal Santé")
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
color: Colors.grey.shade50,
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Journal Émotionnel',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Color(0xFF000080)),
),
Text('Historique de vos saisies (Mobile & Montre)', style: TextStyle(color: Colors.grey, fontSize: 12)),
],
),
),
if (emotionProvider.isLoading)
const Padding(
padding: EdgeInsets.all(20.0),
child: CircularProgressIndicator(),
)
else if (emotionProvider.entries.isEmpty)
const Padding(
padding: EdgeInsets.all(40.0),
child: Column(
children: [
Icon(Icons.history, size: 48, color: Colors.grey),
SizedBox(height: 10),
Text('Aucun historique pour le moment.', style: TextStyle(color: Colors.grey)),
],
),
)
else
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: emotionProvider.entries.length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
final entry = emotionProvider.entries[index];
final dateStr = DateFormat('dd/MM/yyyy HH:mm').format(entry.createdAt);
return ListTile(
leading: _getMoodEmoji(entry.emotion),
title: Text(entry.emotion, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(entry.note ?? 'Saisie rapide'),
trailing: Text(dateStr, style: const TextStyle(fontSize: 11, color: Colors.grey)),
);
},
),
const SizedBox(height: 100), // Espace pour le scroll
],
),
),
);
}
Widget _getMoodEmoji(String mood) {
String emoji = "😐";
Color color = Colors.grey;
switch (mood) {
case 'Très bien':
emoji = "😊";
color = const Color(0xFF4CAF50);
break;
case 'Bien':
emoji = "🙂";
color = const Color(0xFF8BC34A);
break;
case 'Neutre':
emoji = "😐";
color = const Color(0xFFFFC107);
break;
case 'Pas top':
emoji = "🙁";
color = const Color(0xFFFF9800);
break;
case 'Stressé':
emoji = "😫";
color = const Color(0xFFF44336);
break;
}
return CircleAvatar(
backgroundColor: color.withOpacity(0.1),
child: Text(emoji, style: const TextStyle(fontSize: 20)),
);
}
}
@@ -0,0 +1,132 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/auth_provider.dart';
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
void _register() async {
if (_formKey.currentState!.validate()) {
final authProvider = Provider.of<AuthProvider>(context, listen: false);
final success = await authProvider.register(
_nameController.text,
_emailController.text,
_passwordController.text,
_confirmPasswordController.text,
);
if (success) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Compte créé avec succès !')),
);
// On vide la pile et on retourne au menu principal (HomeScreen)
Navigator.of(context).popUntil((route) => route.isFirst);
}
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(authProvider.errorMessage ?? 'Échec de l\'inscription')),
);
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Inscription CESIZen')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: [
const SizedBox(height: 20),
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Nom complet',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
validator: (value) => value == null || value.isEmpty ? 'Entrez votre nom' : null,
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
validator: (value) => value == null || !value.contains('@') ? 'Email invalide' : null,
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Mot de passe',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.lock),
),
obscureText: true,
validator: (value) => value == null || value.length < 8 ? '8 caractères minimum' : null,
),
const SizedBox(height: 16),
TextFormField(
controller: _confirmPasswordController,
decoration: const InputDecoration(
labelText: 'Confirmer le mot de passe',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.lock_outline),
),
obscureText: true,
validator: (value) {
if (value != _passwordController.text) return 'Les mots de passe ne correspondent pas';
return null;
},
),
const SizedBox(height: 24),
Consumer<AuthProvider>(
builder: (context, auth, child) {
return auth.isLoading
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: _register,
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(50),
),
child: const Text('S\'inscrire'),
);
},
),
],
),
),
),
);
}
}
@@ -0,0 +1,130 @@
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../../../core/network/dio_client.dart';
import '../models/user.dart';
class AuthService {
final Dio _dio = DioClient().dio;
final FlutterSecureStorage _storage = const FlutterSecureStorage();
Future<Map<String, dynamic>> login(String email, String password) async {
try {
final response = await _dio.post('/login', data: {
'email': email,
'password': password,
});
if (response.statusCode == 200) {
String token = response.data['token'];
await _storage.write(key: 'token', value: token);
return {
'status': 'success',
'user': User.fromJson(response.data['user']),
};
}
return {'status': 'error', 'message': 'Erreur inconnue'};
} on DioException catch (e) {
return {
'status': 'error',
'message': e.response?.data['message'] ?? 'Erreur de connexion'
};
}
}
Future<Map<String, dynamic>> register(String name, String email, String password, String passwordConfirmation) async {
try {
final response = await _dio.post('/register', data: {
'name': name,
'email': email,
'password': password,
'password_confirmation': passwordConfirmation,
});
if (response.statusCode == 201) {
String token = response.data['token'];
await _storage.write(key: 'token', value: token);
return {
'status': 'success',
'user': User.fromJson(response.data['user']),
};
}
return {'status': 'error', 'message': 'Erreur inconnue'};
} on DioException catch (e) {
return {
'status': 'error',
'errors': e.response?.data['errors'] ?? 'Erreur lors de l\'inscription'
};
}
}
Future<Map<String, dynamic>> updateProfile(String name, String email, {String? password}) async {
try {
String? token = await getToken();
final Map<String, dynamic> data = {
'name': name,
'email': email,
};
if (password != null && password.isNotEmpty) {
data['current_password'] = password;
data['password'] = password;
data['password_confirmation'] = password;
}
final response = await _dio.post(
'/update-profile',
data: data,
options: Options(
headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/json',
'Content-Type': 'application/json',
},
),
);
if (response.statusCode == 200) {
return {
'status': 'success',
'user': User.fromJson(response.data['user']),
};
}
return {'status': 'error', 'message': 'Erreur lors de la mise à jour'};
} on DioException catch (e) {
return {
'status': 'error',
'message': e.response?.data['message'] ?? 'Erreur lors de la modification du profil'
};
}
}
Future<Map<String, dynamic>> getProfile() async {
try {
String? token = await getToken();
if (token == null) return {'status': 'error', 'message': 'No token'};
final response = await _dio.get('/profile', options: Options(
headers: {'Authorization': 'Bearer $token'}
));
if (response.statusCode == 200) {
return {
'status': 'success',
'user': User.fromJson(response.data),
};
}
return {'status': 'error', 'message': 'Session expirée'};
} catch (e) {
return {'status': 'error', 'message': e.toString()};
}
}
Future<void> logout() async {
await _storage.delete(key: 'token');
}
Future<String?> getToken() async {
return await _storage.read(key: 'token');
}
}
@@ -0,0 +1,13 @@
class Question {
final String text;
final List<Option> options;
Question({required this.text, required this.options});
}
class Option {
final String text;
final int points;
Option({required this.text, required this.points});
}
@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import '../models/question.dart';
import '../services/diagnostic_service.dart';
class DiagnosticProvider with ChangeNotifier {
final DiagnosticService _service = DiagnosticService();
List<Question> _questions = [];
int _currentQuestionIndex = 0;
int _totalScore = 0;
bool _isFinished = false;
bool _isLoading = true;
List<Question> get questions => _questions;
int get currentQuestionIndex => _currentQuestionIndex;
int get totalScore => _totalScore;
bool get isFinished => _isFinished;
bool get isLoading => _isLoading;
DiagnosticProvider() {
loadQuestions();
}
Future<void> loadQuestions() async {
_isLoading = true;
notifyListeners();
_questions = await _service.fetchStressEvents();
_isLoading = false;
notifyListeners();
}
void answerQuestion(int points) {
_totalScore += points;
if (_currentQuestionIndex < _questions.length - 1) {
_currentQuestionIndex++;
} else {
_isFinished = true;
}
notifyListeners();
}
void reset() {
_currentQuestionIndex = 0;
_totalScore = 0;
_isFinished = false;
notifyListeners();
}
String get stressLevel {
if (_totalScore < 150) return "Risque Faible";
if (_totalScore < 300) return "Risque Modéré (50%)";
return "Risque Élevé (80%)";
}
String get advice {
if (_totalScore < 150) {
return "Votre niveau de changement de vie est gérable. Continuez à maintenir votre équilibre actuel.";
}
if (_totalScore < 300) {
return "Vous avez vécu beaucoup de changements. Il y a un risque modéré que cela affecte votre santé. Prenez du temps pour vous reposer.";
}
return "Attention : Votre score est très élevé. L'accumulation de stress lié à ces changements majeurs pourrait sérieusement impacter votre santé. Il est recommandé de consulter un professionnel.";
}
}
@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/diagnostic_provider.dart';
class DiagnosticScreen extends StatelessWidget {
const DiagnosticScreen({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => DiagnosticProvider(),
child: const _DiagnosticView(),
);
}
}
class _DiagnosticView extends StatelessWidget {
const _DiagnosticView();
@override
Widget build(BuildContext context) {
final provider = Provider.of<DiagnosticProvider>(context);
if (provider.isLoading) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
if (provider.questions.isEmpty) {
return Scaffold(
appBar: AppBar(title: const Text('Diagnostic')),
body: const Center(child: Text('Aucun événement trouvé.')),
);
}
final question = provider.questions[provider.currentQuestionIndex];
if (provider.isFinished) {
return Scaffold(
appBar: AppBar(title: const Text('Résultat du Diagnostic')),
body: Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Votre niveau de stress est :', style: TextStyle(fontSize: 18)),
const SizedBox(height: 10),
Text(
provider.stressLevel,
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: _getColorForLevel(provider.stressLevel),
),
),
const SizedBox(height: 20),
Text(
provider.advice,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16, fontStyle: FontStyle.italic),
),
const SizedBox(height: 40),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('Retour à l\'accueil'),
),
],
),
),
),
);
}
return Scaffold(
appBar: AppBar(
title: Text('Question ${provider.currentQuestionIndex + 1}/${provider.questions.length}'),
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
LinearProgressIndicator(
value: (provider.currentQuestionIndex + 1) / provider.questions.length,
backgroundColor: Colors.grey.shade200,
),
const SizedBox(height: 40),
Text(
question.text,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
textAlign: TextAlign.center,
),
const SizedBox(height: 40),
...question.options.map((option) => Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: ElevatedButton(
onPressed: () => provider.answerQuestion(option.points),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
child: Text(option.text),
),
)),
],
),
),
);
}
Color _getColorForLevel(String level) {
switch (level) {
case "Faible": return Colors.green;
case "Modéré": return Colors.orange;
case "Élevé": return Colors.red;
case "Très élevé": return Colors.red.shade900;
default: return Colors.black;
}
}
}
@@ -0,0 +1,30 @@
import 'package:dio/dio.dart';
import '../../../core/network/dio_client.dart';
import '../models/question.dart';
class DiagnosticService {
final Dio _dio = DioClient().dio;
Future<List<Question>> fetchStressEvents() async {
try {
final response = await _dio.get('/stress-events');
if (response.statusCode == 200) {
List<dynamic> data = response.data;
return data.map((item) {
return Question(
text: "Avez-vous vécu : ${item['event_name']} ?",
options: [
Option(text: "Oui", points: int.parse(item['points'].toString())),
Option(text: "Non", points: 0),
],
);
}).toList();
}
return [];
} catch (e) {
print("Erreur lors de la récupération des événements : $e");
return [];
}
}
}
@@ -0,0 +1,307 @@
import 'dart:async';
import 'package:flutter/material.dart';
class BreathingMode {
final String name;
final String description;
final int inhale;
final int hold;
final int exhale;
final Color color;
BreathingMode({
required this.name,
required this.description,
required this.inhale,
required this.hold,
required this.exhale,
required this.color,
});
}
class BreathingScreen extends StatefulWidget {
const BreathingScreen({super.key});
@override
State<BreathingScreen> createState() => _BreathingScreenState();
}
class _BreathingScreenState extends State<BreathingScreen> with TickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
final List<BreathingMode> _modes = [
BreathingMode(
name: "7-4-8",
description: "Sommeil & Détente profonde",
inhale: 7,
hold: 4,
exhale: 8,
color: Colors.indigo,
),
BreathingMode(
name: "5-5",
description: "Cohérence Cardiaque classique",
inhale: 5,
hold: 0,
exhale: 5,
color: Colors.teal,
),
BreathingMode(
name: "4-6",
description: "Réduction rapide du stress",
inhale: 4,
hold: 0,
exhale: 6,
color: Colors.orange,
),
];
late BreathingMode _selectedMode;
bool _isStarted = false;
String _actionText = "Prêt ?";
int _secondsRemaining = 0;
Timer? _timer;
int _phaseSecondsRemaining = 0;
@override
void initState() {
super.initState();
_selectedMode = _modes[1]; // 5-5 par défaut
_setupController();
}
void _setupController() {
_controller = AnimationController(vsync: this);
_animation = Tween<double>(begin: 0.6, end: 1.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
Future<void> _runExercise() async {
while (_isStarted && _secondsRemaining > 0) {
// Phase 1 : Inspiration
if (!_isStarted) break;
setState(() => _actionText = "Inspirez");
_controller.duration = Duration(seconds: _selectedMode.inhale);
_controller.forward();
await _waitForPhase(_selectedMode.inhale);
// Phase 2 : Apnée (Hold)
if (!_isStarted) break;
if (_selectedMode.hold > 0) {
setState(() => _actionText = "Bloquez");
await _waitForPhase(_selectedMode.hold);
}
// Phase 3 : Expiration
if (!_isStarted) break;
setState(() => _actionText = "Expirez");
_controller.duration = Duration(seconds: _selectedMode.exhale);
_controller.reverse();
await _waitForPhase(_selectedMode.exhale);
}
if (_isStarted) _stopExercise();
}
Future<void> _waitForPhase(int seconds) async {
_phaseSecondsRemaining = seconds;
for (int i = 0; i < seconds; i++) {
if (!_isStarted) return;
await Future.delayed(const Duration(seconds: 1));
if (mounted) setState(() => _phaseSecondsRemaining--);
}
}
void _startExercise() {
setState(() {
_isStarted = true;
_secondsRemaining = 300; // 5 minutes
});
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_secondsRemaining > 0) {
setState(() => _secondsRemaining--);
} else {
_stopExercise();
}
});
_runExercise();
}
void _stopExercise() {
_timer?.cancel();
_controller.stop();
_controller.reset();
setState(() {
_isStarted = false;
_actionText = "Prêt ?";
});
}
@override
void dispose() {
_timer?.cancel();
_controller.dispose();
super.dispose();
}
String _formatTime(int seconds) {
int mins = seconds ~/ 60;
int secs = seconds % 60;
return "$mins:${secs.toString().padLeft(2, '0')}";
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('Respiration Guidée', style: TextStyle(fontWeight: FontWeight.bold)),
elevation: 0,
backgroundColor: Colors.white,
),
body: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
if (!_isStarted) ...[
const SizedBox(height: 20),
const Text(
"Choisissez votre exercice",
style: TextStyle(fontSize: 18, color: Colors.grey),
),
const SizedBox(height: 20),
..._modes.map((mode) => _buildModeCard(mode)).toList(),
] else ...[
const SizedBox(height: 40),
Text(
_formatTime(_secondsRemaining),
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold, letterSpacing: 2),
),
const Spacer(),
_buildAnimatedCircle(),
const SizedBox(height: 20),
if (_phaseSecondsRemaining > 0)
Text(
"$_phaseSecondsRemaining",
style: TextStyle(fontSize: 40, fontWeight: FontWeight.w300, color: _selectedMode.color),
),
const Spacer(),
ElevatedButton(
onPressed: _stopExercise,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade50,
foregroundColor: Colors.red,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 15),
),
child: const Text("ARRÊTER LA SESSION", style: TextStyle(fontWeight: FontWeight.bold)),
),
const SizedBox(height: 40),
]
],
),
),
);
}
Widget _buildModeCard(BreathingMode mode) {
final isSelected = _selectedMode == mode;
return GestureDetector(
onTap: () => setState(() => _selectedMode = mode),
child: Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isSelected ? mode.color.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isSelected ? mode.color : Colors.grey.shade200,
width: isSelected ? 2 : 1,
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: mode.color.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(Icons.air, color: mode.color),
),
const SizedBox(width: 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(mode.name, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Text(mode.description, style: TextStyle(color: Colors.grey.shade600, fontSize: 13)),
],
),
),
if (isSelected)
ElevatedButton(
onPressed: _startExercise,
style: ElevatedButton.styleFrom(
backgroundColor: mode.color,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
child: const Text("GO"),
),
],
),
),
);
}
Widget _buildAnimatedCircle() {
return ScaleTransition(
scale: _animation,
child: Container(
width: 250,
height: 250,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
_selectedMode.color.withOpacity(0.2),
_selectedMode.color.withOpacity(0.05),
],
),
border: Border.all(color: _selectedMode.color.withOpacity(0.3), width: 8),
),
child: Stack(
alignment: Alignment.center,
children: [
// Logo de méditation en fond
Opacity(
opacity: 0.1,
child: Image.asset(
'assets/images/CesiZen logo.png',
width: 150,
),
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_actionText,
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: _selectedMode.color,
),
),
],
),
],
),
),
);
}
}
@@ -0,0 +1,28 @@
class Activity {
final int id;
final String title;
final String description;
final String category; // 'Méditation', 'Musique', 'Respiration', etc.
final String? imageUrl;
final String? videoUrl;
Activity({
required this.id,
required this.title,
required this.description,
required this.category,
this.imageUrl,
this.videoUrl,
});
factory Activity.fromJson(Map<String, dynamic> json) {
return Activity(
id: json['id'],
title: json['title'] ?? '',
description: json['description'] ?? '',
category: json['category'] ?? 'Détente',
imageUrl: json['image_url'],
videoUrl: json['video_url'],
);
}
}
@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import '../models/activity.dart';
import '../services/relaxation_service.dart';
class RelaxationProvider with ChangeNotifier {
final RelaxationService _service = RelaxationService();
List<Activity> _activities = [];
bool _isLoading = false;
String _selectedCategory = 'Toutes';
List<Activity> get activities {
if (_selectedCategory == 'Toutes') return _activities;
return _activities.where((a) => a.category == _selectedCategory).toList();
}
bool get isLoading => _isLoading;
String get selectedCategory => _selectedCategory;
Future<void> loadActivities() async {
_isLoading = true;
notifyListeners();
_activities = await _service.fetchActivities();
_isLoading = false;
notifyListeners();
}
void setCategory(String category) {
_selectedCategory = category;
notifyListeners();
}
}
@@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/relaxation_provider.dart';
class RelaxationScreen extends StatefulWidget {
const RelaxationScreen({super.key});
@override
State<RelaxationScreen> createState() => _RelaxationScreenState();
}
class _RelaxationScreenState extends State<RelaxationScreen> {
@override
void initState() {
super.initState();
Future.microtask(() =>
Provider.of<RelaxationProvider>(context, listen: false).loadActivities());
}
@override
Widget build(BuildContext context) {
final provider = Provider.of<RelaxationProvider>(context);
final categories = ['Toutes', 'Méditation', 'Musique', 'Lecture', 'Sport'];
return Scaffold(
appBar: AppBar(
title: const Text('Espace Détente'),
elevation: 0,
),
body: Column(
children: [
// Barre de catégories
Container(
height: 60,
padding: const EdgeInsets.symmetric(vertical: 10),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: categories.length,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemBuilder: (context, index) {
final cat = categories[index];
final isSelected = provider.selectedCategory == cat;
return Padding(
padding: const EdgeInsets.only(right: 10),
child: FilterChip(
label: Text(cat),
selected: isSelected,
onSelected: (_) => provider.setCategory(cat),
selectedColor: const Color(0xFF000080).withOpacity(0.2),
checkmarkColor: const Color(0xFF000080),
),
);
},
),
),
// Liste des activités
Expanded(
child: provider.isLoading
? const Center(child: CircularProgressIndicator())
: provider.activities.isEmpty
? const Center(child: Text('Aucune activité trouvée.'))
: ListView.builder(
itemCount: provider.activities.length,
padding: const EdgeInsets.all(16),
itemBuilder: (context, index) {
final activity = provider.activities[index];
return Card(
margin: const EdgeInsets.only(bottom: 16),
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (activity.imageUrl != null)
Image.network(
activity.imageUrl!,
height: 150,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
height: 150,
color: Colors.grey.shade200,
child: const Icon(Icons.image_not_supported),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
activity.category,
style: TextStyle(
color: Colors.orange.shade700,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
if (activity.videoUrl != null)
const Icon(Icons.play_circle_fill, color: Color(0xFF000080)),
],
),
const SizedBox(height: 8),
Text(
activity.title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
activity.description,
style: const TextStyle(color: Colors.grey),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
);
},
),
),
],
),
);
}
}
@@ -0,0 +1,21 @@
import 'package:dio/dio.dart';
import '../../../core/network/dio_client.dart';
import '../models/activity.dart';
class RelaxationService {
final Dio _dio = DioClient().dio;
Future<List<Activity>> fetchActivities() async {
try {
final response = await _dio.get('/activities');
if (response.statusCode == 200) {
List<dynamic> data = response.data;
return data.map((item) => Activity.fromJson(item)).toList();
}
return [];
} catch (e) {
print("Erreur fetchActivities: $e");
return [];
}
}
}
@@ -0,0 +1,29 @@
class EmotionEntry {
final int? id;
final String emotion; // ex: 'Heureux', 'Triste', 'Stressé'
final String? note;
final DateTime createdAt;
EmotionEntry({
this.id,
required this.emotion,
this.note,
required this.createdAt,
});
factory EmotionEntry.fromJson(Map<String, dynamic> json) {
return EmotionEntry(
id: json['id'],
emotion: json['emotion_name'] ?? json['emotion'] ?? '',
note: json['note'],
createdAt: DateTime.parse(json['created_at']),
);
}
Map<String, dynamic> toJson() {
return {
'emotion_name': emotion,
'note': note,
};
}
}
@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import '../models/emotion_entry.dart';
import '../services/emotion_service.dart';
class EmotionProvider with ChangeNotifier {
final EmotionService _service = EmotionService();
List<EmotionEntry> _entries = [];
bool _isLoading = false;
List<EmotionEntry> get entries => _entries;
bool get isLoading => _isLoading;
Future<void> loadEmotions() async {
_isLoading = true;
notifyListeners();
_entries = await _service.fetchEmotions();
// Trier par date décroissante (plus récent en haut)
_entries.sort((a, b) => b.createdAt.compareTo(a.createdAt));
_isLoading = false;
notifyListeners();
}
Future<bool> addEmotion(String emotion, String note) async {
final success = await _service.addEmotion(emotion, note);
if (success) {
await loadEmotions();
}
return success;
}
Future<bool> deleteEmotion(int id) async {
final success = await _service.deleteEmotion(id);
if (success) {
_entries.removeWhere((entry) => entry.id == id);
notifyListeners();
}
return success;
}
}
@@ -0,0 +1,168 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/emotion_provider.dart';
import 'package:intl/intl.dart';
class TrackerScreen extends StatefulWidget {
const TrackerScreen({super.key});
@override
State<TrackerScreen> createState() => _TrackerScreenState();
}
class _TrackerScreenState extends State<TrackerScreen> {
@override
void initState() {
super.initState();
Future.microtask(() =>
Provider.of<EmotionProvider>(context, listen: false).loadEmotions());
}
void _showAddEmotionDialog() {
String selectedEmotion = 'Bien';
final noteController = TextEditingController();
final emotions = ['Très bien', 'Bien', 'Neutre', 'Pas top', 'Stressé'];
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: const Text('Comment vous sentez-vous ?'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
DropdownButton<String>(
value: selectedEmotion,
isExpanded: true,
items: emotions.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (newValue) {
setDialogState(() => selectedEmotion = newValue!);
},
),
const SizedBox(height: 20),
TextField(
controller: noteController,
decoration: const InputDecoration(
labelText: 'Note (optionnel)',
border: OutlineInputBorder(),
),
maxLines: 3,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('ANNULER'),
),
ElevatedButton(
onPressed: () async {
final success = await Provider.of<EmotionProvider>(context, listen: false)
.addEmotion(selectedEmotion, noteController.text);
if (mounted) {
Navigator.pop(context);
if (!success) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Erreur lors de l\'ajout')),
);
}
}
},
child: const Text('ENREGISTRER'),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
final provider = Provider.of<EmotionProvider>(context);
return Scaffold(
appBar: AppBar(title: const Text('Journal des Émotions')),
body: provider.isLoading
? const Center(child: CircularProgressIndicator())
: provider.entries.isEmpty
? const Center(child: Text('Aucune entrée pour le moment.'))
: ListView.builder(
itemCount: provider.entries.length,
padding: const EdgeInsets.all(16),
itemBuilder: (context, index) {
final entry = provider.entries[index];
return Card(
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
child: ListTile(
leading: _getEmotionIcon(entry.emotion),
title: Text(
entry.emotion,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (entry.note != null && entry.note!.isNotEmpty)
Text(entry.note!),
Text(
DateFormat('dd/MM/yyyy HH:mm').format(entry.createdAt),
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
trailing: IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.red),
onPressed: () => provider.deleteEmotion(entry.id!),
),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _showAddEmotionDialog,
backgroundColor: const Color(0xFF000080),
child: const Icon(Icons.add, color: Colors.white),
),
);
}
Widget _getEmotionIcon(String emotion) {
IconData icon;
Color color;
switch (emotion) {
case 'Très bien':
icon = Icons.sentiment_very_satisfied;
color = const Color(0xFF4CAF50);
break;
case 'Bien':
icon = Icons.sentiment_satisfied;
color = const Color(0xFF8BC34A);
break;
case 'Neutre':
icon = Icons.sentiment_neutral;
color = const Color(0xFFFFC107);
break;
case 'Pas top':
icon = Icons.sentiment_dissatisfied;
color = const Color(0xFFFF9800);
break;
case 'Stressé':
icon = Icons.sentiment_very_dissatisfied;
color = const Color(0xFFF44336);
break;
default:
icon = Icons.sentiment_neutral;
color = Colors.grey;
}
return CircleAvatar(
backgroundColor: color.withOpacity(0.1),
child: Icon(icon, color: color),
);
}
}
@@ -0,0 +1,69 @@
import 'package:dio/dio.dart';
import '../../../core/network/dio_client.dart';
import '../../auth/services/auth_service.dart';
import '../models/emotion_entry.dart';
class EmotionService {
final Dio _dio = DioClient().dio;
final AuthService _authService = AuthService();
Future<List<EmotionEntry>> fetchEmotions() async {
try {
String? token = await _authService.getToken();
final response = await _dio.get(
'/emotions',
options: Options(headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/json',
}),
);
if (response.statusCode == 200) {
List<dynamic> data = response.data;
return data.map((item) => EmotionEntry.fromJson(item)).toList();
}
return [];
} catch (e) {
print("Erreur fetchEmotions: $e");
return [];
}
}
Future<bool> addEmotion(String emotion, String note) async {
try {
String? token = await _authService.getToken();
final response = await _dio.post(
'/emotions',
data: {
'emotion_name': emotion,
'note': note,
},
options: Options(headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/json',
}),
);
return response.statusCode == 201 || response.statusCode == 200;
} catch (e) {
print("Erreur addEmotion: $e");
return false;
}
}
Future<bool> deleteEmotion(int id) async {
try {
String? token = await _authService.getToken();
final response = await _dio.delete(
'/emotions/$id',
options: Options(headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/json',
}),
);
return response.statusCode == 200 || response.statusCode == 204;
} catch (e) {
print("Erreur deleteEmotion: $e");
return false;
}
}
}
@@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
import '../tracker/providers/emotion_provider.dart';
class WearSyncService {
static const _platform = MethodChannel('com.cesizen/wearable');
static void initialize(BuildContext context) {
_platform.setMethodCallHandler((call) async {
switch (call.method) {
case "requestStatusUpdate":
// On peut forcer un rafraîchissement des données ici si besoin
break;
case "saveMoodFromWear":
final String mood = call.arguments['mood'];
print("DEBUG FLUTTER: Message saveMoodFromWear reçu pour '$mood'");
if (mood.isNotEmpty) {
// UTILISATION DU PROVIDER : C'est la clé pour que le token soit inclus
try {
final emotionProvider = Provider.of<EmotionProvider>(context, listen: false);
await emotionProvider.addEmotion(mood, "Enregistré depuis la montre");
print("DEBUG: Humeur '$mood' envoyée à l'API via Provider");
} catch (e) {
print("DEBUG ERROR: Impossible d'accéder au Provider: $e");
}
}
break;
}
});
}
static Future<void> syncMoodToWatch(String mood) async {
await _platform.invokeMethod('syncMood', {'mood': mood});
}
static Future<void> checkAndPromptWearSync(BuildContext context, String? userName) async {
if (await Permission.bluetoothConnect.request().isGranted) {
final bool? isWatchNearby = await _platform.invokeMethod('isWatchNearby');
if (isWatchNearby == true && context.mounted) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text("Montre connectée détectée"),
content: const Text(
"Souhaitez-vous lier CESIZen à votre montre pour suivre votre état émotionnel au poignet ?"
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("PLUS TARD", style: TextStyle(color: Colors.grey)),
),
ElevatedButton(
onPressed: () async {
Navigator.pop(context);
await _platform.invokeMethod('sendAuthStatus', {
'status': userName != null ? 'authenticated' : 'unauthenticated',
'userName': userName ?? '',
});
},
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF000091)),
child: const Text("ACTIVER LA LIAISON", style: TextStyle(color: Colors.white)),
),
],
);
},
);
}
}
}
}