feat: export ICS
This commit is contained in:
70
em2rp/docs/EXPORT_CALENDAR.md
Normal file
70
em2rp/docs/EXPORT_CALENDAR.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Export vers Google Calendar
|
||||||
|
|
||||||
|
## Fonctionnalité
|
||||||
|
|
||||||
|
L'application permet d'exporter un événement au format ICS (iCalendar), compatible avec Google Calendar, Apple Calendar, Outlook et la plupart des applications de calendrier.
|
||||||
|
|
||||||
|
## Utilisation
|
||||||
|
|
||||||
|
1. Ouvrir les détails d'un événement
|
||||||
|
2. Cliquer sur l'icône de calendrier 📅 dans l'en-tête
|
||||||
|
3. Le fichier `.ics` sera automatiquement téléchargé
|
||||||
|
4. Ouvrir le fichier pour l'importer dans votre application de calendrier
|
||||||
|
|
||||||
|
## Informations exportées
|
||||||
|
|
||||||
|
Le fichier ICS contient :
|
||||||
|
|
||||||
|
### Informations principales
|
||||||
|
- **Titre** : Nom de l'événement
|
||||||
|
- **Date de début** : Date et heure de début
|
||||||
|
- **Date de fin** : Date et heure de fin
|
||||||
|
- **Lieu** : Adresse de l'événement
|
||||||
|
- **Statut** : Confirmé / Annulé / En attente
|
||||||
|
|
||||||
|
### Description détaillée
|
||||||
|
- Type d'événement
|
||||||
|
- Description complète
|
||||||
|
- Jauge (nombre de personnes)
|
||||||
|
- Email de contact
|
||||||
|
- Téléphone de contact
|
||||||
|
- Temps d'installation et démontage
|
||||||
|
- Liste de la main d'œuvre
|
||||||
|
- Options sélectionnées (avec quantités)
|
||||||
|
- Prix de base
|
||||||
|
|
||||||
|
## Format du fichier
|
||||||
|
|
||||||
|
Le fichier généré suit le standard **RFC 5545** (iCalendar) et est nommé selon le format :
|
||||||
|
```
|
||||||
|
event_[nom_evenement]_[date].ics
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemple : `event_Concert_Mairie_20251225.ics`
|
||||||
|
|
||||||
|
## Compatibilité
|
||||||
|
|
||||||
|
✅ Google Calendar
|
||||||
|
✅ Apple Calendar (macOS, iOS)
|
||||||
|
✅ Microsoft Outlook
|
||||||
|
✅ Thunderbird
|
||||||
|
✅ Autres applications supportant le format ICS
|
||||||
|
|
||||||
|
## Import dans Google Calendar
|
||||||
|
|
||||||
|
1. Télécharger le fichier `.ics`
|
||||||
|
2. Ouvrir Google Calendar
|
||||||
|
3. Cliquer sur l'icône ⚙️ (Paramètres)
|
||||||
|
4. Sélectionner "Importation et exportation"
|
||||||
|
5. Cliquer sur "Sélectionner un fichier sur votre ordinateur"
|
||||||
|
6. Choisir le fichier `.ics` téléchargé
|
||||||
|
7. Sélectionner le calendrier de destination
|
||||||
|
8. Cliquer sur "Importer"
|
||||||
|
|
||||||
|
## Notes techniques
|
||||||
|
|
||||||
|
- Les dates sont converties en UTC pour assurer la compatibilité internationale
|
||||||
|
- Les caractères spéciaux sont correctement échappés selon le standard ICS
|
||||||
|
- Un UID unique est généré pour chaque événement (`em2rp-[eventId]@em2rp.app`)
|
||||||
|
- Le fichier est encodé en UTF-8
|
||||||
|
|
||||||
18
em2rp/docs/example_event.ics
Normal file
18
em2rp/docs/example_event.ics
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//EM2RP//Event Manager//FR
|
||||||
|
CALSCALE:GREGORIAN
|
||||||
|
METHOD:PUBLISH
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:em2rp-example123@em2rp.app
|
||||||
|
DTSTAMP:20251220T120000Z
|
||||||
|
DTSTART:20251225T190000Z
|
||||||
|
DTEND:20251225T230000Z
|
||||||
|
SUMMARY:Concert de Noël
|
||||||
|
DESCRIPTION:TYPE: Concert\n\nDESCRIPTION:\nConcert de Noël avec orchestre symphonique et chorale.\n\nJAUGE: 500 personnes\nEMAIL DE CONTACT: contact@example.com\nTÉLÉPHONE DE CONTACT: 06 12 34 56 78\n\nADRESSE: Salle des fêtes\, Place de la Mairie\, 75001 Paris\n\nINSTALLATION: 4h\nDÉMONTAGE: 2h\n\nMAIN D'ŒUVRE:\n - Jean Dupont\n - Marie Martin\n - Pierre Durand\n\nOPTIONS:\n - Système son professionnel\n - Éclairage scénique (x2)\n\nPRIX DE BASE: 2500.00€\n\n---\nGéré par EM2RP Event Manager
|
||||||
|
LOCATION:Salle des fêtes\, Place de la Mairie\, 75001 Paris
|
||||||
|
STATUS:CONFIRMED
|
||||||
|
CATEGORIES:Concert
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ class EventFormController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initialize([EventModel? existingEvent]) async {
|
Future<void> initialize({EventModel? existingEvent, DateTime? selectedDate}) async {
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
_fetchUsers(),
|
_fetchUsers(),
|
||||||
_fetchEventTypes(),
|
_fetchEventTypes(),
|
||||||
@@ -92,6 +92,20 @@ class EventFormController extends ChangeNotifier {
|
|||||||
_populateFromEvent(existingEvent);
|
_populateFromEvent(existingEvent);
|
||||||
} else {
|
} else {
|
||||||
_selectedStatus = EventStatus.waitingForApproval;
|
_selectedStatus = EventStatus.waitingForApproval;
|
||||||
|
|
||||||
|
// Préremplir les dates si une date est sélectionnée dans le calendrier
|
||||||
|
if (selectedDate != null) {
|
||||||
|
// Date de début : selectedDate à 20h00
|
||||||
|
_startDateTime = DateTime(
|
||||||
|
selectedDate.year,
|
||||||
|
selectedDate.month,
|
||||||
|
selectedDate.day,
|
||||||
|
20,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
// Date de fin : selectedDate + 4 heures
|
||||||
|
_endDateTime = _startDateTime!.add(const Duration(hours: 4));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|||||||
267
em2rp/lib/services/ics_export_service.dart
Normal file
267
em2rp/lib/services/ics_export_service.dart
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
import 'package:em2rp/models/event_model.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
|
||||||
|
class IcsExportService {
|
||||||
|
/// Génère un fichier ICS à partir d'un événement
|
||||||
|
static Future<String> generateIcsContent(EventModel event) async {
|
||||||
|
final now = DateTime.now().toUtc();
|
||||||
|
final timestamp = DateFormat('yyyyMMddTHHmmss').format(now) + 'Z';
|
||||||
|
|
||||||
|
// Récupérer les informations supplémentaires
|
||||||
|
final eventTypeName = await _getEventTypeName(event.eventTypeId);
|
||||||
|
final workforce = await _getWorkforceDetails(event.workforce);
|
||||||
|
final optionsWithNames = await _getOptionsDetails(event.options);
|
||||||
|
|
||||||
|
// Formater les dates au format ICS (UTC)
|
||||||
|
final startDate = _formatDateForIcs(event.startDateTime);
|
||||||
|
final endDate = _formatDateForIcs(event.endDateTime);
|
||||||
|
|
||||||
|
// Construire la description détaillée
|
||||||
|
final description = _buildDescription(event, eventTypeName, workforce, optionsWithNames);
|
||||||
|
|
||||||
|
// Générer un UID unique basé sur l'ID de l'événement
|
||||||
|
final uid = 'em2rp-${event.id}@em2rp.app';
|
||||||
|
|
||||||
|
// Construire le contenu ICS
|
||||||
|
final icsContent = '''BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//EM2RP//Event Manager//FR
|
||||||
|
CALSCALE:GREGORIAN
|
||||||
|
METHOD:PUBLISH
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:$uid
|
||||||
|
DTSTAMP:$timestamp
|
||||||
|
DTSTART:$startDate
|
||||||
|
DTEND:$endDate
|
||||||
|
SUMMARY:${_escapeIcsText(event.name)}
|
||||||
|
DESCRIPTION:${_escapeIcsText(description)}
|
||||||
|
LOCATION:${_escapeIcsText(event.address)}
|
||||||
|
STATUS:${_getEventStatus(event.status)}
|
||||||
|
CATEGORIES:${_escapeIcsText(eventTypeName)}
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR''';
|
||||||
|
|
||||||
|
return icsContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupère le nom du type d'événement
|
||||||
|
static Future<String> _getEventTypeName(String eventTypeId) async {
|
||||||
|
if (eventTypeId.isEmpty) return 'Non spécifié';
|
||||||
|
|
||||||
|
try {
|
||||||
|
final doc = await FirebaseFirestore.instance
|
||||||
|
.collection('eventTypes')
|
||||||
|
.doc(eventTypeId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (doc.exists) {
|
||||||
|
return doc.data()?['name'] as String? ?? eventTypeId;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Erreur lors de la récupération du type d\'événement: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventTypeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupère les détails de la main d'œuvre
|
||||||
|
static Future<List<String>> _getWorkforceDetails(List<DocumentReference> workforce) async {
|
||||||
|
final List<String> workforceNames = [];
|
||||||
|
|
||||||
|
for (final ref in workforce) {
|
||||||
|
try {
|
||||||
|
final doc = await ref.get();
|
||||||
|
if (doc.exists) {
|
||||||
|
final data = doc.data() as Map<String, dynamic>?;
|
||||||
|
if (data != null) {
|
||||||
|
final firstName = data['firstName'] ?? '';
|
||||||
|
final lastName = data['lastName'] ?? '';
|
||||||
|
if (firstName.isNotEmpty || lastName.isNotEmpty) {
|
||||||
|
workforceNames.add('$firstName $lastName'.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Erreur lors de la récupération des détails utilisateur: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return workforceNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupère les détails des options
|
||||||
|
static Future<List<Map<String, dynamic>>> _getOptionsDetails(List<Map<String, dynamic>> options) async {
|
||||||
|
final List<Map<String, dynamic>> optionsWithNames = [];
|
||||||
|
|
||||||
|
for (final option in options) {
|
||||||
|
try {
|
||||||
|
final optionId = option['id'] ?? option['optionId'];
|
||||||
|
if (optionId == null || optionId.toString().isEmpty) {
|
||||||
|
// Si pas d'ID, garder le nom tel quel
|
||||||
|
optionsWithNames.add({
|
||||||
|
'name': option['name'] ?? 'Option inconnue',
|
||||||
|
'quantity': option['quantity'],
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer le nom depuis Firestore
|
||||||
|
final doc = await FirebaseFirestore.instance
|
||||||
|
.collection('options')
|
||||||
|
.doc(optionId.toString())
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (doc.exists) {
|
||||||
|
final data = doc.data();
|
||||||
|
optionsWithNames.add({
|
||||||
|
'name': data?['name'] ?? option['name'] ?? 'Option inconnue',
|
||||||
|
'quantity': option['quantity'],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Document n'existe pas, garder le nom de l'option
|
||||||
|
optionsWithNames.add({
|
||||||
|
'name': option['name'] ?? 'Option inconnue',
|
||||||
|
'quantity': option['quantity'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Erreur lors de la récupération des détails option: $e');
|
||||||
|
optionsWithNames.add({
|
||||||
|
'name': option['name'] ?? 'Option inconnue',
|
||||||
|
'quantity': option['quantity'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return optionsWithNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit la description détaillée de l'événement
|
||||||
|
static String _buildDescription(
|
||||||
|
EventModel event,
|
||||||
|
String eventTypeName,
|
||||||
|
List<String> workforce,
|
||||||
|
List<Map<String, dynamic>> optionsWithNames,
|
||||||
|
) {
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
|
||||||
|
// Type d'événement
|
||||||
|
buffer.writeln('TYPE: $eventTypeName');
|
||||||
|
buffer.writeln('');
|
||||||
|
|
||||||
|
// Description
|
||||||
|
if (event.description.isNotEmpty) {
|
||||||
|
buffer.writeln('DESCRIPTION:');
|
||||||
|
buffer.writeln(event.description);
|
||||||
|
buffer.writeln('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jauge
|
||||||
|
if (event.jauge != null) {
|
||||||
|
buffer.writeln('JAUGE: ${event.jauge} personnes');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contact email
|
||||||
|
if (event.contactEmail != null && event.contactEmail!.isNotEmpty) {
|
||||||
|
buffer.writeln('EMAIL DE CONTACT: ${event.contactEmail}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contact téléphone
|
||||||
|
if (event.contactPhone != null && event.contactPhone!.isNotEmpty) {
|
||||||
|
buffer.writeln('TÉLÉPHONE DE CONTACT: ${event.contactPhone}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adresse
|
||||||
|
if (event.address.isNotEmpty) {
|
||||||
|
buffer.writeln('');
|
||||||
|
buffer.writeln('ADRESSE: ${event.address}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temps d'installation et démontage
|
||||||
|
if (event.installationTime > 0 || event.disassemblyTime > 0) {
|
||||||
|
buffer.writeln('');
|
||||||
|
if (event.installationTime > 0) {
|
||||||
|
buffer.writeln('INSTALLATION: ${event.installationTime}h');
|
||||||
|
}
|
||||||
|
if (event.disassemblyTime > 0) {
|
||||||
|
buffer.writeln('DÉMONTAGE: ${event.disassemblyTime}h');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main d'œuvre
|
||||||
|
if (workforce.isNotEmpty) {
|
||||||
|
buffer.writeln('');
|
||||||
|
buffer.writeln('MAIN D\'ŒUVRE:');
|
||||||
|
for (final name in workforce) {
|
||||||
|
buffer.writeln(' - $name');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options
|
||||||
|
if (optionsWithNames.isNotEmpty) {
|
||||||
|
buffer.writeln('');
|
||||||
|
buffer.writeln('OPTIONS:');
|
||||||
|
for (final option in optionsWithNames) {
|
||||||
|
final optionName = option['name'] ?? 'Option inconnue';
|
||||||
|
final quantity = option['quantity'];
|
||||||
|
if (quantity != null && quantity > 1) {
|
||||||
|
buffer.writeln(' - $optionName (x$quantity)');
|
||||||
|
} else {
|
||||||
|
buffer.writeln(' - $optionName');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prix
|
||||||
|
if (event.basePrice > 0) {
|
||||||
|
buffer.writeln('');
|
||||||
|
buffer.writeln('PRIX DE BASE: ${event.basePrice.toStringAsFixed(2)}€');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lien vers l'application
|
||||||
|
buffer.writeln('');
|
||||||
|
buffer.writeln('---');
|
||||||
|
buffer.writeln('Géré par EM2RP Event Manager');
|
||||||
|
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formate une date au format ICS (yyyyMMddTHHmmssZ)
|
||||||
|
static String _formatDateForIcs(DateTime dateTime) {
|
||||||
|
final utcDate = dateTime.toUtc();
|
||||||
|
return DateFormat('yyyyMMddTHHmmss').format(utcDate) + 'Z';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Échappe les caractères spéciaux pour le format ICS
|
||||||
|
static String _escapeIcsText(String text) {
|
||||||
|
return text
|
||||||
|
.replaceAll('\\', '\\\\')
|
||||||
|
.replaceAll(',', '\\,')
|
||||||
|
.replaceAll(';', '\\;')
|
||||||
|
.replaceAll('\n', '\\n')
|
||||||
|
.replaceAll('\r', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convertit le statut de l'événement en statut ICS
|
||||||
|
static String _getEventStatus(EventStatus status) {
|
||||||
|
switch (status) {
|
||||||
|
case EventStatus.confirmed:
|
||||||
|
return 'CONFIRMED';
|
||||||
|
case EventStatus.canceled:
|
||||||
|
return 'CANCELLED';
|
||||||
|
case EventStatus.waitingForApproval:
|
||||||
|
return 'TENTATIVE';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Génère le nom du fichier ICS
|
||||||
|
static String generateFileName(EventModel event) {
|
||||||
|
final safeName = event.name
|
||||||
|
.replaceAll(RegExp(r'[^\w\s-]'), '')
|
||||||
|
.replaceAll(RegExp(r'\s+'), '_');
|
||||||
|
final dateStr = DateFormat('yyyyMMdd').format(event.startDateTime);
|
||||||
|
return 'event_${safeName}_$dateStr.ics';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -130,7 +130,9 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const EventAddEditPage(),
|
builder: (context) => EventAddEditPage(
|
||||||
|
selectedDate: _selectedDay ?? DateTime.now(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import 'package:em2rp/views/widgets/inputs/option_selector_widget.dart';
|
|||||||
|
|
||||||
class EventAddEditPage extends StatefulWidget {
|
class EventAddEditPage extends StatefulWidget {
|
||||||
final EventModel? event;
|
final EventModel? event;
|
||||||
const EventAddEditPage({super.key, this.event});
|
final DateTime? selectedDate;
|
||||||
|
|
||||||
|
const EventAddEditPage({super.key, this.event, this.selectedDate});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<EventAddEditPage> createState() => _EventAddEditPageState();
|
State<EventAddEditPage> createState() => _EventAddEditPageState();
|
||||||
@@ -27,7 +29,10 @@ class _EventAddEditPageState extends State<EventAddEditPage> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller = EventFormController();
|
_controller = EventFormController();
|
||||||
_controller.initialize(widget.event);
|
_controller.initialize(
|
||||||
|
existingEvent: widget.event,
|
||||||
|
selectedDate: widget.selectedDate,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import 'package:em2rp/utils/colors.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:em2rp/providers/local_user_provider.dart';
|
import 'package:em2rp/providers/local_user_provider.dart';
|
||||||
import 'package:em2rp/views/event_add_page.dart';
|
import 'package:em2rp/views/event_add_page.dart';
|
||||||
|
import 'package:em2rp/services/ics_export_service.dart';
|
||||||
|
import 'dart:html' as html;
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
class EventDetailsHeader extends StatefulWidget {
|
class EventDetailsHeader extends StatefulWidget {
|
||||||
final EventModel event;
|
final EventModel event;
|
||||||
@@ -93,6 +96,12 @@ class _EventDetailsHeaderState extends State<EventDetailsHeader> {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
_buildStatusIcon(widget.event.status),
|
_buildStatusIcon(widget.event.status),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.calendar_today, color: AppColors.rouge),
|
||||||
|
tooltip: 'Exporter vers Google Calendar',
|
||||||
|
onPressed: _exportToCalendar,
|
||||||
|
),
|
||||||
if (Provider.of<LocalUserProvider>(context, listen: false)
|
if (Provider.of<LocalUserProvider>(context, listen: false)
|
||||||
.hasPermission('edit_event')) ...[
|
.hasPermission('edit_event')) ...[
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
@@ -112,6 +121,63 @@ class _EventDetailsHeaderState extends State<EventDetailsHeader> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _exportToCalendar() async {
|
||||||
|
try {
|
||||||
|
// Afficher un indicateur de chargement
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
||||||
|
),
|
||||||
|
SizedBox(width: 16),
|
||||||
|
Text('Génération du fichier ICS...'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Générer le contenu ICS
|
||||||
|
final icsContent = await IcsExportService.generateIcsContent(widget.event);
|
||||||
|
final fileName = IcsExportService.generateFileName(widget.event);
|
||||||
|
|
||||||
|
// Créer un blob et télécharger le fichier
|
||||||
|
final bytes = utf8.encode(icsContent);
|
||||||
|
final blob = html.Blob([bytes], 'text/calendar');
|
||||||
|
final url = html.Url.createObjectUrlFromBlob(blob);
|
||||||
|
html.AnchorElement(href: url)
|
||||||
|
..setAttribute('download', fileName)
|
||||||
|
..click();
|
||||||
|
html.Url.revokeObjectUrl(url);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Événement exporté : $fileName'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
action: SnackBarAction(
|
||||||
|
label: 'OK',
|
||||||
|
textColor: Colors.white,
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erreur lors de l\'export : $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildStatusIcon(EventStatus status) {
|
Widget _buildStatusIcon(EventStatus status) {
|
||||||
Color color;
|
Color color;
|
||||||
IconData icon;
|
IconData icon;
|
||||||
|
|||||||
@@ -181,16 +181,24 @@ class EventBasicInfoSection extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _selectStartDateTime(BuildContext context) async {
|
Future<void> _selectStartDateTime(BuildContext context) async {
|
||||||
|
// Utiliser la date actuelle de l'événement ou aujourd'hui
|
||||||
|
final initialDate = startDateTime ?? DateTime.now();
|
||||||
|
|
||||||
final picked = await showDatePicker(
|
final picked = await showDatePicker(
|
||||||
context: context,
|
context: context,
|
||||||
initialDate: DateTime.now(),
|
initialDate: initialDate,
|
||||||
firstDate: DateTime(2020),
|
firstDate: DateTime(2020),
|
||||||
lastDate: DateTime(2099),
|
lastDate: DateTime(2099),
|
||||||
);
|
);
|
||||||
if (picked != null) {
|
if (picked != null) {
|
||||||
|
// Utiliser l'heure actuelle de l'événement ou l'heure actuelle
|
||||||
|
final initialTime = startDateTime != null
|
||||||
|
? TimeOfDay(hour: startDateTime!.hour, minute: startDateTime!.minute)
|
||||||
|
: TimeOfDay.now();
|
||||||
|
|
||||||
final time = await showTimePicker(
|
final time = await showTimePicker(
|
||||||
context: context,
|
context: context,
|
||||||
initialTime: TimeOfDay.now(),
|
initialTime: initialTime,
|
||||||
);
|
);
|
||||||
if (time != null) {
|
if (time != null) {
|
||||||
final newDateTime = DateTime(
|
final newDateTime = DateTime(
|
||||||
@@ -206,16 +214,24 @@ class EventBasicInfoSection extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _selectEndDateTime(BuildContext context) async {
|
Future<void> _selectEndDateTime(BuildContext context) async {
|
||||||
|
// Utiliser la date actuelle de fin ou date de début + 1h
|
||||||
|
final initialDate = endDateTime ?? startDateTime!.add(const Duration(hours: 1));
|
||||||
|
|
||||||
final picked = await showDatePicker(
|
final picked = await showDatePicker(
|
||||||
context: context,
|
context: context,
|
||||||
initialDate: startDateTime!.add(const Duration(hours: 1)),
|
initialDate: initialDate,
|
||||||
firstDate: startDateTime!,
|
firstDate: startDateTime!,
|
||||||
lastDate: DateTime(2099),
|
lastDate: DateTime(2099),
|
||||||
);
|
);
|
||||||
if (picked != null) {
|
if (picked != null) {
|
||||||
|
// Utiliser l'heure actuelle de l'événement ou l'heure actuelle
|
||||||
|
final initialTime = endDateTime != null
|
||||||
|
? TimeOfDay(hour: endDateTime!.hour, minute: endDateTime!.minute)
|
||||||
|
: TimeOfDay.now();
|
||||||
|
|
||||||
final time = await showTimePicker(
|
final time = await showTimePicker(
|
||||||
context: context,
|
context: context,
|
||||||
initialTime: TimeOfDay.now(),
|
initialTime: initialTime,
|
||||||
);
|
);
|
||||||
if (time != null) {
|
if (time != null) {
|
||||||
final newDateTime = DateTime(
|
final newDateTime = DateTime(
|
||||||
|
|||||||
Reference in New Issue
Block a user