Ajout du choix des utilisateurs sur un événement. Ajout de fichiers dans un événement. (dropzone cassée)
This commit is contained in:
@ -1,11 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:em2rp/providers/event_provider.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:em2rp/views/widgets/inputs/int_stepper_field.dart';
|
||||
import 'package:em2rp/models/user_model.dart';
|
||||
import 'package:em2rp/views/widgets/image/profile_picture.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:firebase_storage/firebase_storage.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:em2rp/providers/local_user_provider.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_dropzone/flutter_dropzone.dart';
|
||||
|
||||
class EventAddPage extends StatefulWidget {
|
||||
const EventAddPage({super.key});
|
||||
@ -21,8 +32,7 @@ class _EventAddPageState extends State<EventAddPage> {
|
||||
final TextEditingController _priceController = TextEditingController();
|
||||
final TextEditingController _installationController = TextEditingController();
|
||||
final TextEditingController _disassemblyController = TextEditingController();
|
||||
final TextEditingController _latitudeController = TextEditingController();
|
||||
final TextEditingController _longitudeController = TextEditingController();
|
||||
final TextEditingController _addressController = TextEditingController();
|
||||
DateTime? _startDateTime;
|
||||
DateTime? _endDateTime;
|
||||
bool _isLoading = false;
|
||||
@ -31,11 +41,18 @@ class _EventAddPageState extends State<EventAddPage> {
|
||||
String? _selectedEventType;
|
||||
final List<String> _eventTypes = ['Bal', 'Mariage', 'Anniversaire'];
|
||||
int _descriptionMaxLines = 3;
|
||||
List<String> _selectedUserIds = [];
|
||||
List<UserModel> _allUsers = [];
|
||||
bool _isLoadingUsers = true;
|
||||
List<Map<String, String>> _uploadedFiles = [];
|
||||
DropzoneViewController? _dropzoneController;
|
||||
bool _isDropzoneHighlighted = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_descriptionController.addListener(_handleDescriptionChange);
|
||||
_fetchUsers();
|
||||
}
|
||||
|
||||
void _handleDescriptionChange() {
|
||||
@ -45,6 +62,16 @@ class _EventAddPageState extends State<EventAddPage> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _fetchUsers() async {
|
||||
final snapshot = await FirebaseFirestore.instance.collection('users').get();
|
||||
setState(() {
|
||||
_allUsers = snapshot.docs
|
||||
.map((doc) => UserModel.fromMap(doc.data(), doc.id))
|
||||
.toList();
|
||||
_isLoadingUsers = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
@ -52,16 +79,93 @@ class _EventAddPageState extends State<EventAddPage> {
|
||||
_priceController.dispose();
|
||||
_installationController.dispose();
|
||||
_disassemblyController.dispose();
|
||||
_latitudeController.dispose();
|
||||
_longitudeController.dispose();
|
||||
_addressController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _pickAndUploadFiles() async {
|
||||
final result = await FilePicker.platform
|
||||
.pickFiles(allowMultiple: true, withData: true);
|
||||
if (result != null && result.files.isNotEmpty) {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
List<Map<String, String>> files = [];
|
||||
for (final file in result.files) {
|
||||
final fileBytes = file.bytes;
|
||||
final fileName = file.name;
|
||||
if (fileBytes != null) {
|
||||
final ref = FirebaseStorage.instance.ref().child(
|
||||
'events/temp/${DateTime.now().millisecondsSinceEpoch}_$fileName');
|
||||
final uploadTask = await ref.putData(fileBytes);
|
||||
final url = await uploadTask.ref.getDownloadURL();
|
||||
files.add({'name': fileName, 'url': url});
|
||||
} else {
|
||||
setState(() {
|
||||
_error = "Impossible de lire le fichier ${file.name}";
|
||||
});
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_uploadedFiles.addAll(files);
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = 'Erreur lors de l\'upload : $e';
|
||||
});
|
||||
} finally {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> moveEventFileHttp({
|
||||
required String sourcePath,
|
||||
required String destinationPath,
|
||||
}) async {
|
||||
final url = Uri.parse(
|
||||
'https://us-central1-em2rp-951dc.cloudfunctions.net/moveEventFileV2');
|
||||
final user = FirebaseAuth.instance.currentUser;
|
||||
final idToken = await user?.getIdToken();
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
if (idToken != null) 'Authorization': 'Bearer $idToken',
|
||||
},
|
||||
body: jsonEncode({
|
||||
'data': {
|
||||
'sourcePath': sourcePath,
|
||||
'destinationPath': destinationPath,
|
||||
}
|
||||
}),
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
if (data['url'] != null) {
|
||||
return data['url'] as String;
|
||||
} else if (data['result'] != null && data['result']['url'] != null) {
|
||||
return data['result']['url'] as String;
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
print('Erreur Cloud Function: \\n${response.body}');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (!_formKey.currentState!.validate() ||
|
||||
_startDateTime == null ||
|
||||
_endDateTime == null ||
|
||||
_selectedEventType == null) return;
|
||||
_selectedEventType == null ||
|
||||
_addressController.text.isEmpty) return;
|
||||
if (_endDateTime!.isBefore(_startDateTime!) ||
|
||||
_endDateTime!.isAtSameMomentAs(_startDateTime!)) {
|
||||
setState(() {
|
||||
_error = "La date de fin doit être postérieure à la date de début.";
|
||||
});
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
@ -79,14 +183,54 @@ class _EventAddPageState extends State<EventAddPage> {
|
||||
installationTime: int.tryParse(_installationController.text) ?? 0,
|
||||
disassemblyTime: int.tryParse(_disassemblyController.text) ?? 0,
|
||||
eventTypeId: _selectedEventType!,
|
||||
customerId: '', // à adapter si tu veux gérer les clients
|
||||
address: LatLng(
|
||||
double.tryParse(_latitudeController.text) ?? 0.0,
|
||||
double.tryParse(_longitudeController.text) ?? 0.0,
|
||||
),
|
||||
workforce: [],
|
||||
customerId: '',
|
||||
address: _addressController.text.trim(),
|
||||
workforce: _selectedUserIds
|
||||
.map((id) => FirebaseFirestore.instance.collection('users').doc(id))
|
||||
.toList(),
|
||||
latitude: 0.0,
|
||||
longitude: 0.0,
|
||||
documents: _uploadedFiles,
|
||||
);
|
||||
await eventProvider.addEvent(newEvent);
|
||||
final docRef = await FirebaseFirestore.instance
|
||||
.collection('events')
|
||||
.add(newEvent.toMap());
|
||||
final eventId = docRef.id;
|
||||
List<Map<String, String>> newFiles = [];
|
||||
for (final file in _uploadedFiles) {
|
||||
final fileName = file['name']!;
|
||||
final oldUrl = file['url']!;
|
||||
String sourcePath;
|
||||
final tempPattern = RegExp(r'events/temp/[^?]+');
|
||||
final match = tempPattern.firstMatch(oldUrl);
|
||||
if (match != null) {
|
||||
sourcePath = match.group(0)!;
|
||||
} else {
|
||||
final tempFileName =
|
||||
Uri.decodeComponent(oldUrl.split('/').last.split('?').first);
|
||||
sourcePath = tempFileName;
|
||||
}
|
||||
final destinationPath = 'events/$eventId/$fileName';
|
||||
final newUrl = await moveEventFileHttp(
|
||||
sourcePath: sourcePath,
|
||||
destinationPath: destinationPath,
|
||||
);
|
||||
if (newUrl != null) {
|
||||
newFiles.add({'name': fileName, 'url': newUrl});
|
||||
} else {
|
||||
newFiles.add({'name': fileName, 'url': oldUrl});
|
||||
}
|
||||
}
|
||||
await docRef.update({'documents': newFiles});
|
||||
final localUserProvider =
|
||||
Provider.of<LocalUserProvider>(context, listen: false);
|
||||
final userId = localUserProvider.uid;
|
||||
final canViewAllEvents =
|
||||
localUserProvider.hasPermission('view_all_events');
|
||||
if (userId != null) {
|
||||
await eventProvider.loadUserEvents(userId,
|
||||
canViewAllEvents: canViewAllEvents);
|
||||
}
|
||||
setState(() {
|
||||
_success = "Événement créé avec succès !";
|
||||
});
|
||||
@ -203,6 +347,13 @@ class _EventAddPageState extends State<EventAddPage> {
|
||||
time.hour,
|
||||
time.minute,
|
||||
);
|
||||
if (_endDateTime != null &&
|
||||
(_endDateTime!
|
||||
.isBefore(_startDateTime!) ||
|
||||
_endDateTime!.isAtSameMomentAs(
|
||||
_startDateTime!))) {
|
||||
_endDateTime = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -232,31 +383,34 @@ class _EventAddPageState extends State<EventAddPage> {
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _startDateTime ?? DateTime.now(),
|
||||
firstDate: DateTime(2020),
|
||||
lastDate: DateTime(2099),
|
||||
);
|
||||
if (picked != null) {
|
||||
final time = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.now(),
|
||||
);
|
||||
if (time != null) {
|
||||
setState(() {
|
||||
_endDateTime = DateTime(
|
||||
picked.year,
|
||||
picked.month,
|
||||
picked.day,
|
||||
time.hour,
|
||||
time.minute,
|
||||
onTap: _startDateTime == null
|
||||
? null
|
||||
: () async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _startDateTime!
|
||||
.add(const Duration(hours: 1)),
|
||||
firstDate: _startDateTime!,
|
||||
lastDate: DateTime(2099),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
if (picked != null) {
|
||||
final time = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.now(),
|
||||
);
|
||||
if (time != null) {
|
||||
setState(() {
|
||||
_endDateTime = DateTime(
|
||||
picked.year,
|
||||
picked.month,
|
||||
picked.day,
|
||||
time.hour,
|
||||
time.minute,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
child: AbsorbPointer(
|
||||
child: TextFormField(
|
||||
readOnly: true,
|
||||
@ -274,7 +428,14 @@ class _EventAddPageState extends State<EventAddPage> {
|
||||
),
|
||||
validator: (v) => _endDateTime == null
|
||||
? 'Champ requis'
|
||||
: null,
|
||||
: (_startDateTime != null &&
|
||||
_endDateTime != null &&
|
||||
(_endDateTime!.isBefore(
|
||||
_startDateTime!) ||
|
||||
_endDateTime!.isAtSameMomentAs(
|
||||
_startDateTime!)))
|
||||
? 'La date de fin doit être après la date de début'
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -288,8 +449,28 @@ class _EventAddPageState extends State<EventAddPage> {
|
||||
labelText: 'Prix (€)',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.euro),
|
||||
hintText: '1050.50',
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp(r'^\d*\.?\d{0,2}')),
|
||||
],
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Le prix est requis';
|
||||
}
|
||||
final price =
|
||||
double.tryParse(value.replaceAll(',', '.'));
|
||||
if (price == null) {
|
||||
return 'Veuillez entrer un nombre valide';
|
||||
}
|
||||
if (price < 0) {
|
||||
return 'Le prix ne peut pas être négatif';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
_buildSectionTitle('Détails'),
|
||||
AnimatedContainer(
|
||||
@ -319,7 +500,7 @@ class _EventAddPageState extends State<EventAddPage> {
|
||||
label: 'Installation (h)',
|
||||
controller: _installationController,
|
||||
min: 0,
|
||||
max: 24,
|
||||
max: 99,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
@ -328,51 +509,369 @@ class _EventAddPageState extends State<EventAddPage> {
|
||||
label: 'Démontage (h)',
|
||||
controller: _disassemblyController,
|
||||
min: 0,
|
||||
max: 24,
|
||||
max: 99,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildSectionTitle('Localisation'),
|
||||
_buildSectionTitle('Adresse'),
|
||||
TextFormField(
|
||||
controller: _addressController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Adresse',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.location_on),
|
||||
),
|
||||
validator: (v) =>
|
||||
v == null || v.isEmpty ? 'Champ requis' : null,
|
||||
),
|
||||
_buildSectionTitle('Personnel'),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _latitudeController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Latitude',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.location_on),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
child: _isLoadingUsers
|
||||
? const CircularProgressIndicator()
|
||||
: UserMultiSelect(
|
||||
allUsers: _allUsers,
|
||||
selectedUserIds: _selectedUserIds,
|
||||
onChanged: (ids) =>
|
||||
setState(() => _selectedUserIds = ids),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _longitudeController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Longitude',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.location_on),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionTitle('Documents'),
|
||||
if (kIsWeb)
|
||||
Container(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 180, minWidth: 200),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: _isDropzoneHighlighted
|
||||
? Colors.blue
|
||||
: Colors.grey,
|
||||
width: 2,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: _isDropzoneHighlighted
|
||||
? Colors.blue.withOpacity(0.1)
|
||||
: null,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
DropzoneView(
|
||||
onCreated: (controller) =>
|
||||
_dropzoneController = controller,
|
||||
onDrop: (ev) async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
final name =
|
||||
await _dropzoneController!
|
||||
.getFilename(ev);
|
||||
final bytes =
|
||||
await _dropzoneController!
|
||||
.getFileData(ev);
|
||||
if (bytes != null) {
|
||||
final ref = FirebaseStorage
|
||||
.instance
|
||||
.ref()
|
||||
.child(
|
||||
'events/temp/${DateTime.now().millisecondsSinceEpoch}_$name');
|
||||
final uploadTask =
|
||||
await ref.putData(bytes);
|
||||
final url = await uploadTask.ref
|
||||
.getDownloadURL();
|
||||
setState(() {
|
||||
_uploadedFiles.add(
|
||||
{'name': name, 'url': url});
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error =
|
||||
'Erreur lors de l\'upload : $e';
|
||||
});
|
||||
} finally {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_isDropzoneHighlighted = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
onHover: () => setState(() =>
|
||||
_isDropzoneHighlighted = true),
|
||||
onLeave: () => setState(() =>
|
||||
_isDropzoneHighlighted = false),
|
||||
onDropMultiple: (evs) async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
if (evs != null) {
|
||||
for (final ev in evs) {
|
||||
final name =
|
||||
await _dropzoneController!
|
||||
.getFilename(ev);
|
||||
final bytes =
|
||||
await _dropzoneController!
|
||||
.getFileData(ev);
|
||||
if (bytes != null) {
|
||||
final ref = FirebaseStorage
|
||||
.instance
|
||||
.ref()
|
||||
.child(
|
||||
'events/temp/${DateTime.now().millisecondsSinceEpoch}_$name');
|
||||
final uploadTask =
|
||||
await ref.putData(bytes);
|
||||
final url = await uploadTask
|
||||
.ref
|
||||
.getDownloadURL();
|
||||
setState(() {
|
||||
_uploadedFiles.add({
|
||||
'name': name,
|
||||
'url': url
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error =
|
||||
'Erreur lors de l\'upload : $e';
|
||||
});
|
||||
} finally {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_isDropzoneHighlighted = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: _uploadedFiles.isEmpty
|
||||
? Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.cloud_upload,
|
||||
size: 48,
|
||||
color: Colors.grey),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Glissez-déposez des fichiers ici ou cliquez sur "Ajouter"',
|
||||
textAlign:
|
||||
TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
if (_uploadedFiles.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: _buildFileListAndButton(),
|
||||
),
|
||||
if (_uploadedFiles.isEmpty)
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 160,
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(
|
||||
Icons.attach_file,
|
||||
size: 18),
|
||||
label: const Text('Ajouter'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets
|
||||
.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 8),
|
||||
minimumSize:
|
||||
const Size(80, 36),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 14),
|
||||
),
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: _pickAndUploadFiles,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_error != null)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(top: 8.0),
|
||||
child: Text(_error!,
|
||||
style: const TextStyle(
|
||||
color: Colors.red)),
|
||||
),
|
||||
),
|
||||
if (_success != null)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 32.0),
|
||||
child: Text(_success!,
|
||||
style: const TextStyle(
|
||||
color: Colors.green)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else if ([
|
||||
TargetPlatform.windows,
|
||||
TargetPlatform.macOS,
|
||||
TargetPlatform.linux
|
||||
].contains(defaultTargetPlatform))
|
||||
DragTarget<PlatformFile>(
|
||||
onWillAccept: (data) => true,
|
||||
onAccept: (file) async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
final fileBytes = file.bytes;
|
||||
final fileName = file.name;
|
||||
if (fileBytes != null) {
|
||||
final ref = FirebaseStorage.instance
|
||||
.ref()
|
||||
.child(
|
||||
'events/temp/${DateTime.now().millisecondsSinceEpoch}_$fileName');
|
||||
final uploadTask =
|
||||
await ref.putData(fileBytes);
|
||||
final url = await uploadTask.ref
|
||||
.getDownloadURL();
|
||||
setState(() {
|
||||
_uploadedFiles.add(
|
||||
{'name': fileName, 'url': url});
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error =
|
||||
'Erreur lors de l\'upload : $e';
|
||||
});
|
||||
} finally {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
},
|
||||
builder:
|
||||
(context, candidateData, rejectedData) {
|
||||
final hasFiles = _uploadedFiles.isNotEmpty;
|
||||
return Container(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 180, minWidth: 200),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: candidateData.isNotEmpty
|
||||
? Colors.blue
|
||||
: Colors.grey,
|
||||
width: 2,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: candidateData.isNotEmpty
|
||||
? Colors.blue.withOpacity(0.1)
|
||||
: null,
|
||||
),
|
||||
child: hasFiles
|
||||
? _buildFileListAndButton()
|
||||
: Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.cloud_upload,
|
||||
size: 48,
|
||||
color: Colors.grey),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Glissez-déposez des fichiers ici ou cliquez sur "Ajouter"',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: 160,
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(
|
||||
Icons.attach_file,
|
||||
size: 18),
|
||||
label:
|
||||
const Text('Ajouter'),
|
||||
style: ElevatedButton
|
||||
.styleFrom(
|
||||
padding: const EdgeInsets
|
||||
.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 8),
|
||||
minimumSize:
|
||||
const Size(80, 36),
|
||||
textStyle:
|
||||
const TextStyle(
|
||||
fontSize: 14),
|
||||
),
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: _pickAndUploadFiles,
|
||||
),
|
||||
),
|
||||
if (_error != null)
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(
|
||||
top: 8.0),
|
||||
child: Text(_error!,
|
||||
style: const TextStyle(
|
||||
color: Colors.red)),
|
||||
),
|
||||
if (_success != null)
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(
|
||||
top: 8.0),
|
||||
child: Text(_success!,
|
||||
style: const TextStyle(
|
||||
color:
|
||||
Colors.green)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
else
|
||||
_buildFileListAndButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(_error!,
|
||||
style: const TextStyle(color: Colors.red)),
|
||||
),
|
||||
if (_success != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(_success!,
|
||||
style: const TextStyle(color: Colors.green)),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
@ -407,4 +906,219 @@ class _EventAddPageState extends State<EventAddPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFileListAndButton() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
..._uploadedFiles.map((file) {
|
||||
final fileName = file['name']!;
|
||||
final ext = p.extension(fileName).toLowerCase();
|
||||
IconData icon;
|
||||
if ([".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"]
|
||||
.contains(ext)) {
|
||||
icon = Icons.image;
|
||||
} else if (ext == ".pdf") {
|
||||
icon = Icons.picture_as_pdf;
|
||||
} else if ([".txt", ".md", ".csv", ".json", ".xml"].contains(ext)) {
|
||||
icon = Icons.description;
|
||||
} else {
|
||||
icon = Icons.attach_file;
|
||||
}
|
||||
return ListTile(
|
||||
leading: Icon(icon, color: Colors.blueGrey),
|
||||
title: Text(fileName, overflow: TextOverflow.ellipsis),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_uploadedFiles.remove(file);
|
||||
});
|
||||
},
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
);
|
||||
}).toList(),
|
||||
SizedBox(
|
||||
width: 160,
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.attach_file, size: 18),
|
||||
label: const Text('Ajouter'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
minimumSize: const Size(80, 36),
|
||||
textStyle: const TextStyle(fontSize: 14),
|
||||
),
|
||||
onPressed: _isLoading ? null : _pickAndUploadFiles,
|
||||
),
|
||||
),
|
||||
if (_error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(_error!, style: const TextStyle(color: Colors.red)),
|
||||
),
|
||||
if (_success != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(_success!, style: const TextStyle(color: Colors.green)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UserMultiSelect extends StatefulWidget {
|
||||
final List<UserModel> allUsers;
|
||||
final List<String> selectedUserIds;
|
||||
final ValueChanged<List<String>> onChanged;
|
||||
|
||||
const UserMultiSelect({
|
||||
super.key,
|
||||
required this.allUsers,
|
||||
required this.selectedUserIds,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<UserMultiSelect> createState() => _UserMultiSelectState();
|
||||
}
|
||||
|
||||
class _UserMultiSelectState extends State<UserMultiSelect> {
|
||||
void _openUserPicker() async {
|
||||
final result = await showDialog<List<String>>(
|
||||
context: context,
|
||||
builder: (context) => _UserPickerDialog(
|
||||
allUsers: widget.allUsers,
|
||||
initiallySelected: widget.selectedUserIds,
|
||||
),
|
||||
);
|
||||
if (result != null) {
|
||||
widget.onChanged(result);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedUsers = widget.allUsers
|
||||
.where((u) => widget.selectedUserIds.contains(u.uid))
|
||||
.toList();
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: selectedUsers
|
||||
.map((user) => Chip(
|
||||
avatar: ProfilePictureWidget(userId: user.uid, radius: 28),
|
||||
label: Text('${user.firstName} ${user.lastName}',
|
||||
style: const TextStyle(fontSize: 16)),
|
||||
labelPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
deleteIcon: const Icon(Icons.close, size: 20),
|
||||
onDeleted: () {
|
||||
final newList = List<String>.from(widget.selectedUserIds)
|
||||
..remove(user.uid);
|
||||
widget.onChanged(newList);
|
||||
},
|
||||
backgroundColor: Colors.grey[200],
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Ajouter'),
|
||||
onPressed: _openUserPicker,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UserPickerDialog extends StatefulWidget {
|
||||
final List<UserModel> allUsers;
|
||||
final List<String> initiallySelected;
|
||||
const _UserPickerDialog(
|
||||
{required this.allUsers, required this.initiallySelected});
|
||||
|
||||
@override
|
||||
State<_UserPickerDialog> createState() => _UserPickerDialogState();
|
||||
}
|
||||
|
||||
class _UserPickerDialogState extends State<_UserPickerDialog> {
|
||||
String _search = '';
|
||||
late List<String> _selected;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selected = List<String>.from(widget.initiallySelected);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final filteredUsers = widget.allUsers.where((u) {
|
||||
final query = _search.toLowerCase();
|
||||
return ('${u.firstName} ${u.lastName}').toLowerCase().contains(query);
|
||||
}).toList();
|
||||
return AlertDialog(
|
||||
title: const Text('Ajouter du personnel'),
|
||||
content: SizedBox(
|
||||
width: 400,
|
||||
height: 400,
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Rechercher',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
),
|
||||
onChanged: (v) => setState(() => _search = v),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(
|
||||
child: filteredUsers.isEmpty
|
||||
? const Center(child: Text('Aucun utilisateur trouvé'))
|
||||
: ListView.builder(
|
||||
itemCount: filteredUsers.length,
|
||||
itemBuilder: (context, i) {
|
||||
final user = filteredUsers[i];
|
||||
final isChecked = _selected.contains(user.uid);
|
||||
return CheckboxListTile(
|
||||
value: isChecked,
|
||||
onChanged: (checked) {
|
||||
setState(() {
|
||||
if (checked == true) {
|
||||
_selected.add(user.uid);
|
||||
} else {
|
||||
_selected.remove(user.uid);
|
||||
}
|
||||
});
|
||||
},
|
||||
title: Text('${user.firstName} ${user.lastName}'),
|
||||
subtitle: Text(user.email),
|
||||
secondary: ProfilePictureWidget(
|
||||
userId: user.uid, radius: 20),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, _selected),
|
||||
child: const Text('Ajouter'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user