Trying to do the notification for all users.
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
|
|
||||||
<!-- Permissions pour écrire dans le stockage -->
|
<!-- Permissions pour écrire dans le stockage -->
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<application
|
<application
|
||||||
android:label="Travel Mate"
|
android:label="Travel Mate"
|
||||||
@@ -62,6 +63,9 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.geo.API_KEY"
|
android:name="com.google.android.geo.API_KEY"
|
||||||
android:value="AIzaSyCAtz1_d5K0ANwxAA_T84iq7Ac_gsUs_oM"/>
|
android:value="AIzaSyCAtz1_d5K0ANwxAA_T84iq7Ac_gsUs_oM"/>
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||||
|
android:value="high_importance_channel" />
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
https://developer.android.com/training/package-visibility and
|
https://developer.android.com/training/package-visibility and
|
||||||
|
|||||||
@@ -5,20 +5,31 @@ admin.initializeApp();
|
|||||||
|
|
||||||
// Helper function to send notifications to a list of users
|
// Helper function to send notifications to a list of users
|
||||||
async function sendNotificationToUsers(userIds, title, body, excludeUserId, data = {}) {
|
async function sendNotificationToUsers(userIds, title, body, excludeUserId, data = {}) {
|
||||||
|
console.log(`Starting sendNotificationToUsers. Total users: ${userIds.length}, Exclude: ${excludeUserId}`);
|
||||||
try {
|
try {
|
||||||
const tokens = [];
|
const tokens = [];
|
||||||
|
|
||||||
for (const userId of userIds) {
|
for (const userId of userIds) {
|
||||||
if (userId === excludeUserId) continue;
|
if (userId === excludeUserId) {
|
||||||
|
console.log(`Skipping user ${userId} (sender)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const userDoc = await admin.firestore().collection("users").doc(userId).get();
|
const userDoc = await admin.firestore().collection("users").doc(userId).get();
|
||||||
if (userDoc.exists) {
|
if (userDoc.exists) {
|
||||||
const userData = userDoc.data();
|
const userData = userDoc.data();
|
||||||
if (userData.fcmToken) {
|
if (userData.fcmToken) {
|
||||||
|
console.log(`Found token for user ${userId}`);
|
||||||
tokens.push(userData.fcmToken);
|
tokens.push(userData.fcmToken);
|
||||||
|
} else {
|
||||||
|
console.log(`No FCM token found for user ${userId}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`User document not found for ${userId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
console.log(`Total tokens to send: ${tokens.length}`);
|
||||||
|
|
||||||
if (tokens.length > 0) {
|
if (tokens.length > 0) {
|
||||||
const message = {
|
const message = {
|
||||||
@@ -35,6 +46,11 @@ async function sendNotificationToUsers(userIds, title, body, excludeUserId, data
|
|||||||
|
|
||||||
const response = await admin.messaging().sendMulticast(message);
|
const response = await admin.messaging().sendMulticast(message);
|
||||||
console.log(`${response.successCount} messages were sent successfully`);
|
console.log(`${response.successCount} messages were sent successfully`);
|
||||||
|
if (response.failureCount > 0) {
|
||||||
|
console.log('Failed notifications:', response.responses.filter(r => !r.success));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("No tokens found, skipping notification send.");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error sending notification:", error);
|
console.error("Error sending notification:", error);
|
||||||
@@ -44,6 +60,7 @@ async function sendNotificationToUsers(userIds, title, body, excludeUserId, data
|
|||||||
exports.onActivityCreated = functions.firestore
|
exports.onActivityCreated = functions.firestore
|
||||||
.document("activities/{activityId}")
|
.document("activities/{activityId}")
|
||||||
.onCreate(async (snapshot, context) => {
|
.onCreate(async (snapshot, context) => {
|
||||||
|
console.log(`onActivityCreated triggered for ${context.params.activityId}`);
|
||||||
const activity = snapshot.data();
|
const activity = snapshot.data();
|
||||||
const tripId = activity.tripId;
|
const tripId = activity.tripId;
|
||||||
const createdBy = activity.createdBy || "Unknown";
|
const createdBy = activity.createdBy || "Unknown";
|
||||||
@@ -66,6 +83,8 @@ exports.onActivityCreated = functions.firestore
|
|||||||
participants.push(trip.createdBy);
|
participants.push(trip.createdBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`Found trip participants: ${JSON.stringify(participants)}`);
|
||||||
|
|
||||||
// Fetch creator name
|
// Fetch creator name
|
||||||
let creatorName = "Quelqu'un";
|
let creatorName = "Quelqu'un";
|
||||||
if (createdBy !== "Unknown") {
|
if (createdBy !== "Unknown") {
|
||||||
@@ -87,6 +106,7 @@ exports.onActivityCreated = functions.firestore
|
|||||||
exports.onMessageCreated = functions.firestore
|
exports.onMessageCreated = functions.firestore
|
||||||
.document("groups/{groupId}/messages/{messageId}")
|
.document("groups/{groupId}/messages/{messageId}")
|
||||||
.onCreate(async (snapshot, context) => {
|
.onCreate(async (snapshot, context) => {
|
||||||
|
console.log(`onMessageCreated triggered for ${context.params.messageId} in group ${context.params.groupId}`);
|
||||||
const message = snapshot.data();
|
const message = snapshot.data();
|
||||||
const groupId = context.params.groupId;
|
const groupId = context.params.groupId;
|
||||||
const senderId = message.senderId;
|
const senderId = message.senderId;
|
||||||
@@ -100,6 +120,7 @@ exports.onMessageCreated = functions.firestore
|
|||||||
|
|
||||||
const group = groupDoc.data();
|
const group = groupDoc.data();
|
||||||
const memberIds = group.memberIds || [];
|
const memberIds = group.memberIds || [];
|
||||||
|
console.log(`Found group members: ${JSON.stringify(memberIds)}`);
|
||||||
|
|
||||||
let senderName = message.senderName || "Quelqu'un";
|
let senderName = message.senderName || "Quelqu'un";
|
||||||
|
|
||||||
@@ -115,6 +136,7 @@ exports.onMessageCreated = functions.firestore
|
|||||||
exports.onExpenseCreated = functions.firestore
|
exports.onExpenseCreated = functions.firestore
|
||||||
.document("expenses/{expenseId}")
|
.document("expenses/{expenseId}")
|
||||||
.onCreate(async (snapshot, context) => {
|
.onCreate(async (snapshot, context) => {
|
||||||
|
console.log(`onExpenseCreated triggered for ${context.params.expenseId}`);
|
||||||
const expense = snapshot.data();
|
const expense = snapshot.data();
|
||||||
const groupId = expense.groupId;
|
const groupId = expense.groupId;
|
||||||
const paidBy = expense.paidById || expense.paidBy;
|
const paidBy = expense.paidById || expense.paidBy;
|
||||||
@@ -133,6 +155,7 @@ exports.onExpenseCreated = functions.firestore
|
|||||||
|
|
||||||
const group = groupDoc.data();
|
const group = groupDoc.data();
|
||||||
const memberIds = group.memberIds || [];
|
const memberIds = group.memberIds || [];
|
||||||
|
console.log(`Found group members: ${JSON.stringify(memberIds)}`);
|
||||||
|
|
||||||
let payerName = expense.paidByName || "Quelqu'un";
|
let payerName = expense.paidByName || "Quelqu'un";
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import '../../repositories/auth_repository.dart';
|
import '../../repositories/auth_repository.dart';
|
||||||
import 'auth_event.dart';
|
import 'auth_event.dart';
|
||||||
import 'auth_state.dart';
|
import 'auth_state.dart';
|
||||||
|
import '../../services/notification_service.dart';
|
||||||
|
|
||||||
/// BLoC for managing authentication state and operations.
|
/// BLoC for managing authentication state and operations.
|
||||||
class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||||
@@ -69,6 +70,8 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
|
// Save FCM Token on auto-login
|
||||||
|
await NotificationService().saveTokenToFirestore(user.id!);
|
||||||
emit(AuthAuthenticated(user: user));
|
emit(AuthAuthenticated(user: user));
|
||||||
} else {
|
} else {
|
||||||
emit(AuthUnauthenticated());
|
emit(AuthUnauthenticated());
|
||||||
@@ -98,6 +101,8 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
|
// Save FCM Token
|
||||||
|
await NotificationService().saveTokenToFirestore(user.id!);
|
||||||
emit(AuthAuthenticated(user: user));
|
emit(AuthAuthenticated(user: user));
|
||||||
} else {
|
} else {
|
||||||
emit(const AuthError(message: 'Invalid email or password'));
|
emit(const AuthError(message: 'Invalid email or password'));
|
||||||
@@ -127,6 +132,8 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
|
// Save FCM Token
|
||||||
|
await NotificationService().saveTokenToFirestore(user.id!);
|
||||||
emit(AuthAuthenticated(user: user));
|
emit(AuthAuthenticated(user: user));
|
||||||
} else {
|
} else {
|
||||||
emit(const AuthError(message: 'Failed to create account'));
|
emit(const AuthError(message: 'Failed to create account'));
|
||||||
@@ -150,6 +157,8 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
final user = await _authRepository.signInWithGoogle();
|
final user = await _authRepository.signInWithGoogle();
|
||||||
|
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
|
// Save FCM Token
|
||||||
|
await NotificationService().saveTokenToFirestore(user.id!);
|
||||||
emit(AuthAuthenticated(user: user));
|
emit(AuthAuthenticated(user: user));
|
||||||
} else {
|
} else {
|
||||||
emit(
|
emit(
|
||||||
@@ -224,6 +233,8 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
final user = await _authRepository.signInWithApple();
|
final user = await _authRepository.signInWithApple();
|
||||||
|
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
|
// Save FCM Token
|
||||||
|
await NotificationService().saveTokenToFirestore(user.id!);
|
||||||
emit(AuthAuthenticated(user: user));
|
emit(AuthAuthenticated(user: user));
|
||||||
} else {
|
} else {
|
||||||
emit(
|
emit(
|
||||||
|
|||||||
@@ -86,9 +86,9 @@ class UserBloc extends Bloc<event.UserEvent, state.UserState> {
|
|||||||
// Update FCM token if it changed
|
// Update FCM token if it changed
|
||||||
if (fcmToken != null && user.fcmToken != fcmToken) {
|
if (fcmToken != null && user.fcmToken != fcmToken) {
|
||||||
LoggerService.info('UserBloc - Updating FCM token in Firestore');
|
LoggerService.info('UserBloc - Updating FCM token in Firestore');
|
||||||
await _firestore.collection('users').doc(currentUser.uid).update({
|
await _firestore.collection('users').doc(currentUser.uid).set({
|
||||||
'fcmToken': fcmToken,
|
'fcmToken': fcmToken,
|
||||||
});
|
}, SetOptions(merge: true));
|
||||||
LoggerService.info('UserBloc - FCM token updated');
|
LoggerService.info('UserBloc - FCM token updated');
|
||||||
} else {
|
} else {
|
||||||
LoggerService.info(
|
LoggerService.info(
|
||||||
|
|||||||
@@ -42,49 +42,79 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Formatters for displaying dates and times.
|
// Formatters for displaying dates and times.
|
||||||
final dateFormat = DateFormat('dd MMMM yyyy');
|
final dateFormat = DateFormat('dd MMMM yyyy', 'fr_FR');
|
||||||
final timeFormat = DateFormat('HH:mm');
|
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
return BlocBuilder<UserBloc, user_state.UserState>(
|
return BlocBuilder<UserBloc, user_state.UserState>(
|
||||||
builder: (context, userState) {
|
builder: (context, userState) {
|
||||||
// Determine the current user and their permissions.
|
// Determine the current user and their permissions.
|
||||||
final currentUser = userState is user_state.UserLoaded ? userState.user : null;
|
final currentUser = userState is user_state.UserLoaded
|
||||||
|
? userState.user
|
||||||
|
: null;
|
||||||
final canEdit = currentUser?.id == expense.paidById;
|
final canEdit = currentUser?.id == expense.paidById;
|
||||||
|
|
||||||
return Dialog(
|
return Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
insetPadding: const EdgeInsets.all(16),
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 500, maxHeight: 700),
|
constraints: const BoxConstraints(maxWidth: 500, maxHeight: 700),
|
||||||
child: Scaffold(
|
decoration: BoxDecoration(
|
||||||
appBar: AppBar(
|
color: theme.colorScheme.surface,
|
||||||
title: const Text('Détails de la dépense'),
|
borderRadius: BorderRadius.circular(28),
|
||||||
automaticallyImplyLeading: false,
|
boxShadow: [
|
||||||
actions: [
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.2),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Header with actions
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(24, 20, 16, 0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Détails',
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
if (canEdit) ...[
|
if (canEdit) ...[
|
||||||
// Edit button.
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.edit),
|
icon: const Icon(Icons.edit_outlined),
|
||||||
|
tooltip: 'Modifier',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
_showEditDialog(context, currentUser!);
|
_showEditDialog(context, currentUser!);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
// Delete button.
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete, color: Colors.red),
|
icon: const Icon(
|
||||||
|
Icons.delete_outline,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
tooltip: 'Supprimer',
|
||||||
onPressed: () => _confirmDelete(context),
|
onPressed: () => _confirmDelete(context),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
// Close button.
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: ListView(
|
),
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.fromLTRB(24, 10, 24, 24),
|
||||||
children: [
|
children: [
|
||||||
// Header with icon and description.
|
// Icon and Category
|
||||||
Center(
|
Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -92,30 +122,84 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.blue.withValues(alpha: 0.1),
|
color: theme.colorScheme.primaryContainer
|
||||||
|
.withValues(alpha: 0.3),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
expense.category.icon,
|
expense.category.icon,
|
||||||
size: 40,
|
size: 36,
|
||||||
color: Colors.blue,
|
color: theme.colorScheme.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
expense.description,
|
expense.description,
|
||||||
style: const TextStyle(
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 6,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
theme.colorScheme.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
expense.category.displayName,
|
||||||
|
style: theme.textTheme.labelMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// Amount Display
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceContainerHighest
|
||||||
|
.withValues(alpha: 0.3),
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.colorScheme.outline.withValues(
|
||||||
|
alpha: 0.1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Montant total',
|
||||||
|
style: theme.textTheme.labelLarge?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
expense.category.displayName,
|
'${expense.amount.toStringAsFixed(2)} ${expense.currency.symbol}',
|
||||||
style: TextStyle(
|
style: theme.textTheme.displaySmall?.copyWith(
|
||||||
fontSize: 14,
|
fontWeight: FontWeight.w800,
|
||||||
color: Colors.grey[600],
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (expense.currency != ExpenseCurrency.eur)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: Text(
|
||||||
|
'≈ ${expense.amountInEur.toStringAsFixed(2)} €',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -123,188 +207,289 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Amount card.
|
// Info Grid
|
||||||
Card(
|
Row(
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Expanded(
|
||||||
'${expense.amount.toStringAsFixed(2)} ${expense.currency.symbol}',
|
child: _buildInfoCard(
|
||||||
style: const TextStyle(
|
context,
|
||||||
fontSize: 32,
|
Icons.person_outline,
|
||||||
fontWeight: FontWeight.bold,
|
'Payé par',
|
||||||
color: Colors.green,
|
expense.paidByName,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (expense.currency != ExpenseCurrency.eur)
|
const SizedBox(width: 12),
|
||||||
Text(
|
Expanded(
|
||||||
'≈ ${expense.amountInEur.toStringAsFixed(2)} €',
|
child: _buildInfoCard(
|
||||||
style: TextStyle(
|
context,
|
||||||
fontSize: 16,
|
Icons.calendar_today_outlined,
|
||||||
color: Colors.grey[600],
|
'Date',
|
||||||
|
dateFormat.format(expense.date),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 24),
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Information rows.
|
// Splits Section
|
||||||
_buildInfoRow(Icons.person, 'Payé par', expense.paidByName),
|
Text(
|
||||||
_buildInfoRow(Icons.calendar_today, 'Date', dateFormat.format(expense.date)),
|
|
||||||
_buildInfoRow(Icons.access_time, 'Heure', timeFormat.format(expense.createdAt)),
|
|
||||||
|
|
||||||
if (expense.isEdited && expense.editedAt != null)
|
|
||||||
_buildInfoRow(
|
|
||||||
Icons.edit,
|
|
||||||
'Modifié le',
|
|
||||||
dateFormat.format(expense.editedAt!),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
const Divider(),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
|
|
||||||
// Splits section.
|
|
||||||
const Text(
|
|
||||||
'Répartition',
|
'Répartition',
|
||||||
style: TextStyle(
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
...expense.splits.map((split) => _buildSplitTile(context, split)),
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surface,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.colorScheme.outline.withValues(
|
||||||
|
alpha: 0.1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: expense.splits.asMap().entries.map((entry) {
|
||||||
|
final index = entry.key;
|
||||||
|
final split = entry.value;
|
||||||
|
final isLast = index == expense.splits.length - 1;
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildSplitTile(context, split),
|
||||||
|
if (!isLast)
|
||||||
|
Divider(
|
||||||
|
height: 1,
|
||||||
|
indent: 16,
|
||||||
|
endIndent: 16,
|
||||||
|
color: theme.colorScheme.outline.withValues(
|
||||||
|
alpha: 0.1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
// Receipt Section
|
||||||
|
|
||||||
// Receipt section.
|
|
||||||
if (expense.receiptUrl != null) ...[
|
if (expense.receiptUrl != null) ...[
|
||||||
const Divider(),
|
const SizedBox(height: 24),
|
||||||
const SizedBox(height: 8),
|
Text(
|
||||||
const Text(
|
|
||||||
'Reçu',
|
'Reçu',
|
||||||
style: TextStyle(
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: Image.network(
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Image.network(
|
||||||
expense.receiptUrl!,
|
expense.receiptUrl!,
|
||||||
|
width: double.infinity,
|
||||||
|
height: 200,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
loadingBuilder: (context, child, loadingProgress) {
|
loadingBuilder:
|
||||||
|
(context, child, loadingProgress) {
|
||||||
if (loadingProgress == null) return child;
|
if (loadingProgress == null) return child;
|
||||||
return const Center(child: CircularProgressIndicator());
|
return Container(
|
||||||
|
height: 200,
|
||||||
|
color: theme
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHighest,
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
errorBuilder: (context, error, stackTrace) {
|
errorBuilder: (context, error, stackTrace) {
|
||||||
return const Center(
|
return Container(
|
||||||
child: Text('Erreur de chargement de l\'image'),
|
height: 200,
|
||||||
|
color: theme
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHighest,
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(Icons.broken_image_outlined),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
Positioned.fill(
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
// TODO: Show full screen image
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
// Archive Button
|
||||||
|
if (!expense.isArchived && canEdit) ...[
|
||||||
// Archive button.
|
const SizedBox(height: 32),
|
||||||
if (!expense.isArchived && canEdit)
|
SizedBox(
|
||||||
OutlinedButton.icon(
|
width: double.infinity,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
onPressed: () => _confirmArchive(context),
|
onPressed: () => _confirmArchive(context),
|
||||||
icon: const Icon(Icons.archive),
|
icon: const Icon(Icons.archive_outlined),
|
||||||
label: const Text('Archiver cette dépense'),
|
label: const Text('Archiver cette dépense'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a row displaying an icon, a label, and a value.
|
Widget _buildInfoCard(
|
||||||
Widget _buildInfoRow(IconData icon, String label, String value) {
|
BuildContext context,
|
||||||
return Padding(
|
IconData icon,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
String label,
|
||||||
child: Row(
|
String value,
|
||||||
|
) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surface,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.colorScheme.outline.withValues(alpha: 0.1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 20, color: Colors.grey[600]),
|
Row(
|
||||||
const SizedBox(width: 12),
|
children: [
|
||||||
|
Icon(icon, size: 16, color: theme.colorScheme.onSurfaceVariant),
|
||||||
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: theme.textTheme.labelMedium?.copyWith(
|
||||||
fontSize: 14,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
color: Colors.grey[600],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
value,
|
value,
|
||||||
style: const TextStyle(
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
fontSize: 14,
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSplitTile(BuildContext context, ExpenseSplit split) {
|
||||||
|
return BlocBuilder<UserBloc, user_state.UserState>(
|
||||||
|
builder: (context, userState) {
|
||||||
|
final currentUser = userState is user_state.UserLoaded
|
||||||
|
? userState.user
|
||||||
|
: null;
|
||||||
|
final isCurrentUser = currentUser?.id == split.userId;
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: split.isPaid
|
||||||
|
? Colors.green.withValues(alpha: 0.1)
|
||||||
|
: Colors.orange.withValues(alpha: 0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
split.isPaid ? Icons.check : Icons.access_time_rounded,
|
||||||
|
color: split.isPaid ? Colors.green : Colors.orange,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
isCurrentUser ? 'Moi' : split.userName,
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: isCurrentUser
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
split.isPaid ? 'Payé' : 'En attente',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: split.isPaid ? Colors.green : Colors.orange,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds a tile displaying details about a split in the expense.
|
|
||||||
///
|
|
||||||
/// The tile shows the user's name, the split amount, and whether the split is paid. If the current user
|
|
||||||
/// is responsible for the split and it is unpaid, a button is provided to mark it as paid.
|
|
||||||
Widget _buildSplitTile(BuildContext context, ExpenseSplit split) {
|
|
||||||
return BlocBuilder<UserBloc, user_state.UserState>(
|
|
||||||
builder: (context, userState) {
|
|
||||||
final currentUser = userState is user_state.UserLoaded ? userState.user : null;
|
|
||||||
final isCurrentUser = currentUser?.id == split.userId;
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
leading: CircleAvatar(
|
|
||||||
backgroundColor: split.isPaid ? Colors.green : Colors.orange,
|
|
||||||
child: Icon(
|
|
||||||
split.isPaid ? Icons.check : Icons.pending,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 20,
|
|
||||||
),
|
),
|
||||||
),
|
Column(
|
||||||
title: Text(
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
split.userName,
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: isCurrentUser ? FontWeight.bold : FontWeight.normal,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
subtitle: Text(split.isPaid ? 'Payé' : 'En attente'),
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${split.amount.toStringAsFixed(2)} €',
|
'${split.amount.toStringAsFixed(2)} €',
|
||||||
style: const TextStyle(
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!split.isPaid && isCurrentUser) ...[
|
if (!split.isPaid && isCurrentUser)
|
||||||
const SizedBox(width: 8),
|
GestureDetector(
|
||||||
IconButton(
|
onTap: () {
|
||||||
icon: const Icon(Icons.check_circle, color: Colors.green),
|
context.read<ExpenseBloc>().add(
|
||||||
onPressed: () {
|
MarkSplitAsPaid(
|
||||||
context.read<ExpenseBloc>().add(MarkSplitAsPaid(
|
|
||||||
expenseId: expense.id,
|
expenseId: expense.id,
|
||||||
userId: split.userId,
|
userId: split.userId,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: Text(
|
||||||
|
'Marquer payé',
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -312,7 +497,6 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows a dialog for editing the expense.
|
|
||||||
void _showEditDialog(BuildContext context, user_state.UserModel currentUser) {
|
void _showEditDialog(BuildContext context, user_state.UserModel currentUser) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -327,13 +511,14 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows a confirmation dialog for deleting the expense.
|
|
||||||
void _confirmDelete(BuildContext context) {
|
void _confirmDelete(BuildContext context) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (dialogContext) => AlertDialog(
|
builder: (dialogContext) => AlertDialog(
|
||||||
title: const Text('Supprimer la dépense'),
|
title: const Text('Supprimer la dépense'),
|
||||||
content: const Text('Êtes-vous sûr de vouloir supprimer cette dépense ?'),
|
content: const Text(
|
||||||
|
'Êtes-vous sûr de vouloir supprimer cette dépense ?',
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
@@ -341,9 +526,7 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<ExpenseBloc>().add(DeleteExpense(
|
context.read<ExpenseBloc>().add(DeleteExpense(expense.id));
|
||||||
expense.id,
|
|
||||||
));
|
|
||||||
Navigator.of(dialogContext).pop();
|
Navigator.of(dialogContext).pop();
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
@@ -355,13 +538,14 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows a confirmation dialog for archiving the expense.
|
|
||||||
void _confirmArchive(BuildContext context) {
|
void _confirmArchive(BuildContext context) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (dialogContext) => AlertDialog(
|
builder: (dialogContext) => AlertDialog(
|
||||||
title: const Text('Archiver la dépense'),
|
title: const Text('Archiver la dépense'),
|
||||||
content: const Text('Cette dépense sera archivée et n\'apparaîtra plus dans les calculs de balance.'),
|
content: const Text(
|
||||||
|
'Cette dépense sera archivée et n\'apparaîtra plus dans les calculs de balance.',
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
@@ -369,9 +553,7 @@ class ExpenseDetailDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<ExpenseBloc>().add(ArchiveExpense(
|
context.read<ExpenseBloc>().add(ArchiveExpense(expense.id));
|
||||||
expense.id,
|
|
||||||
));
|
|
||||||
Navigator.of(dialogContext).pop();
|
Navigator.of(dialogContext).pop();
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import '../../blocs/activity/activity_bloc.dart';
|
import '../../blocs/activity/activity_bloc.dart';
|
||||||
import '../../blocs/activity/activity_event.dart';
|
import '../../blocs/activity/activity_event.dart';
|
||||||
import '../../blocs/activity/activity_state.dart';
|
import '../../blocs/activity/activity_state.dart';
|
||||||
@@ -1030,6 +1031,7 @@ class _ActivitiesPageState extends State<ActivitiesPage>
|
|||||||
tripId: widget.trip.id,
|
tripId: widget.trip.id,
|
||||||
// Générer un nouvel ID unique pour cette activité dans le voyage
|
// Générer un nouvel ID unique pour cette activité dans le voyage
|
||||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
createdBy: FirebaseAuth.instance.currentUser?.uid,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Afficher le LoadingContent avec la tâche d'ajout
|
// Afficher le LoadingContent avec la tâche d'ajout
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import '../../blocs/activity/activity_bloc.dart';
|
import '../../blocs/activity/activity_bloc.dart';
|
||||||
import '../../blocs/activity/activity_event.dart';
|
import '../../blocs/activity/activity_event.dart';
|
||||||
@@ -368,6 +369,7 @@ class _AddActivityBottomSheetState extends State<AddActivityBottomSheet> {
|
|||||||
votes: {},
|
votes: {},
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
updatedAt: DateTime.now(),
|
updatedAt: DateTime.now(),
|
||||||
|
createdBy: FirebaseAuth.instance.currentUser?.uid,
|
||||||
);
|
);
|
||||||
|
|
||||||
context.read<ActivityBloc>().add(AddActivity(activity));
|
context.read<ActivityBloc>().add(AddActivity(activity));
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import 'package:travel_mate/services/error_service.dart';
|
|||||||
import 'package:travel_mate/services/activity_places_service.dart';
|
import 'package:travel_mate/services/activity_places_service.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
import 'package:travel_mate/services/expense_service.dart';
|
import 'package:travel_mate/services/expense_service.dart';
|
||||||
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
|
import 'package:travel_mate/services/notification_service.dart';
|
||||||
import 'blocs/auth/auth_bloc.dart';
|
import 'blocs/auth/auth_bloc.dart';
|
||||||
import 'blocs/auth/auth_event.dart';
|
import 'blocs/auth/auth_event.dart';
|
||||||
import 'blocs/theme/theme_bloc.dart';
|
import 'blocs/theme/theme_bloc.dart';
|
||||||
@@ -34,6 +36,8 @@ import 'pages/home.dart';
|
|||||||
import 'pages/signup.dart';
|
import 'pages/signup.dart';
|
||||||
import 'pages/resetpswd.dart';
|
import 'pages/resetpswd.dart';
|
||||||
|
|
||||||
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
|
|
||||||
/// Entry point of the Travel Mate application.
|
/// Entry point of the Travel Mate application.
|
||||||
///
|
///
|
||||||
/// This function initializes Flutter widgets, loads environment variables,
|
/// This function initializes Flutter widgets, loads environment variables,
|
||||||
@@ -42,6 +46,12 @@ void main() async {
|
|||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await dotenv.load(fileName: ".env");
|
await dotenv.load(fileName: ".env");
|
||||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||||
|
await initializeDateFormatting('fr_FR', null);
|
||||||
|
|
||||||
|
// Set the background messaging handler early on, as a named top-level function
|
||||||
|
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
|
||||||
|
|
||||||
|
await NotificationService().initialize();
|
||||||
|
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class Activity {
|
|||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
final DateTime? date; // Date prévue pour l'activité
|
final DateTime? date; // Date prévue pour l'activité
|
||||||
|
final String? createdBy; // ID de l'utilisateur qui a créé l'activité
|
||||||
|
|
||||||
Activity({
|
Activity({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -42,6 +43,7 @@ class Activity {
|
|||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
this.date,
|
this.date,
|
||||||
|
this.createdBy,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Calcule le score total des votes
|
/// Calcule le score total des votes
|
||||||
@@ -108,6 +110,7 @@ class Activity {
|
|||||||
DateTime? updatedAt,
|
DateTime? updatedAt,
|
||||||
DateTime? date,
|
DateTime? date,
|
||||||
bool clearDate = false,
|
bool clearDate = false,
|
||||||
|
String? createdBy,
|
||||||
}) {
|
}) {
|
||||||
return Activity(
|
return Activity(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -129,6 +132,7 @@ class Activity {
|
|||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
updatedAt: updatedAt ?? this.updatedAt,
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
date: clearDate ? null : (date ?? this.date),
|
date: clearDate ? null : (date ?? this.date),
|
||||||
|
createdBy: createdBy ?? this.createdBy,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,6 +158,7 @@ class Activity {
|
|||||||
'createdAt': Timestamp.fromDate(createdAt),
|
'createdAt': Timestamp.fromDate(createdAt),
|
||||||
'updatedAt': Timestamp.fromDate(updatedAt),
|
'updatedAt': Timestamp.fromDate(updatedAt),
|
||||||
'date': date != null ? Timestamp.fromDate(date!) : null,
|
'date': date != null ? Timestamp.fromDate(date!) : null,
|
||||||
|
'createdBy': createdBy,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +184,7 @@ class Activity {
|
|||||||
createdAt: (map['createdAt'] as Timestamp).toDate(),
|
createdAt: (map['createdAt'] as Timestamp).toDate(),
|
||||||
updatedAt: (map['updatedAt'] as Timestamp).toDate(),
|
updatedAt: (map['updatedAt'] as Timestamp).toDate(),
|
||||||
date: map['date'] != null ? (map['date'] as Timestamp).toDate() : null,
|
date: map['date'] != null ? (map['date'] as Timestamp).toDate() : null,
|
||||||
|
createdBy: map['createdBy'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:travel_mate/services/logger_service.dart';
|
import 'package:travel_mate/services/logger_service.dart';
|
||||||
|
|
||||||
|
@pragma('vm:entry-point')
|
||||||
|
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||||
|
await Firebase.initializeApp();
|
||||||
|
LoggerService.info('Handling a background message: ${message.messageId}');
|
||||||
|
}
|
||||||
|
|
||||||
class NotificationService {
|
class NotificationService {
|
||||||
static final NotificationService _instance = NotificationService._internal();
|
static final NotificationService _instance = NotificationService._internal();
|
||||||
factory NotificationService() => _instance;
|
factory NotificationService() => _instance;
|
||||||
@@ -41,8 +49,24 @@ class NotificationService {
|
|||||||
// Handle foreground messages
|
// Handle foreground messages
|
||||||
FirebaseMessaging.onMessage.listen(_handleForegroundMessage);
|
FirebaseMessaging.onMessage.listen(_handleForegroundMessage);
|
||||||
|
|
||||||
|
// Handle token refresh
|
||||||
|
FirebaseMessaging.instance.onTokenRefresh.listen(_onTokenRefresh);
|
||||||
|
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
LoggerService.info('NotificationService initialized');
|
LoggerService.info('NotificationService initialized');
|
||||||
|
|
||||||
|
// Print current token for debugging
|
||||||
|
final token = await getFCMToken();
|
||||||
|
LoggerService.info('Current FCM Token: $token');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onTokenRefresh(String newToken) async {
|
||||||
|
LoggerService.info('FCM Token refreshed: $newToken');
|
||||||
|
// We need the user ID to save the token.
|
||||||
|
// Since this service is a singleton, we might not have direct access to the user ID here
|
||||||
|
// without injecting the repository or bloc.
|
||||||
|
// For now, we rely on the AuthBloc to update the token on login/start.
|
||||||
|
// Ideally, we should save it here if we have the user ID.
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _requestPermissions() async {
|
Future<void> _requestPermissions() async {
|
||||||
@@ -85,6 +109,20 @@ class NotificationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> saveTokenToFirestore(String userId) async {
|
||||||
|
try {
|
||||||
|
final token = await getFCMToken();
|
||||||
|
if (token != null) {
|
||||||
|
await FirebaseFirestore.instance.collection('users').doc(userId).set({
|
||||||
|
'fcmToken': token,
|
||||||
|
}, SetOptions(merge: true));
|
||||||
|
LoggerService.info('FCM Token saved to Firestore for user: $userId');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
LoggerService.error('Error saving FCM token to Firestore: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _handleForegroundMessage(RemoteMessage message) async {
|
Future<void> _handleForegroundMessage(RemoteMessage message) async {
|
||||||
LoggerService.info('Got a message whilst in the foreground!');
|
LoggerService.info('Got a message whilst in the foreground!');
|
||||||
LoggerService.info('Message data: ${message.data}');
|
LoggerService.info('Message data: ${message.data}');
|
||||||
|
|||||||
Reference in New Issue
Block a user