diff --git a/lib/constants.dart b/lib/constants.dart index b57b5e8..a91759f 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -15,6 +15,8 @@ class Constants { static const String dbFieldUsersGender = 'gender'; static const String dbFieldUsersYearBorn = 'born'; + static const String dbFieldUsersSkills = 'skills'; + static const String dbFieldUsersSkillsSought = 'skills_sought'; static const String pathLanguagesJson = 'lib/assets/languages.json'; } diff --git a/lib/enumerations.dart b/lib/enumerations.dart new file mode 100644 index 0000000..8f90f8e --- /dev/null +++ b/lib/enumerations.dart @@ -0,0 +1,31 @@ +enum SkillOption { + product, + finance, + engineering, + design, + marketing, + management, + operations, + legal; + + String get displayName { + switch (this) { + case SkillOption.product: + return 'Product Development'; + case SkillOption.design: + return 'Design'; + case SkillOption.engineering: + return 'Engineering'; + case SkillOption.marketing: + return 'Sales and Marketing'; + case SkillOption.finance: + return 'Finance'; + case SkillOption.management: + return 'Management'; + case SkillOption.operations: + return 'Operations'; + case SkillOption.legal: + return 'Legal'; + } + } +} diff --git a/lib/forms/profile_category_form.dart b/lib/forms/profile_category_form.dart new file mode 100644 index 0000000..ff5b247 --- /dev/null +++ b/lib/forms/profile_category_form.dart @@ -0,0 +1,153 @@ +import 'package:flutter/material.dart'; +import '../../helper.dart'; + +class ProfileCategoryForm extends StatefulWidget { + final String title; + final String description; + final List options; // T to make it work with our different Enums + final int minSelections; + final int maxSelections; + final Function(List) onSave; + final List preSelectedOptions; + final String? saveButtonText; + final bool hideSaveButton; + + const ProfileCategoryForm({ + super.key, + required this.title, + required this.description, + required this.options, + required this.minSelections, + required this.maxSelections, + required this.onSave, + required this.preSelectedOptions, + this.saveButtonText, + this.hideSaveButton = false, // Initialize hideSaveButton parameter + }); + + @override + ProfileCategoryFormState createState() => ProfileCategoryFormState(); +} + +class ProfileCategoryFormState extends State> { + late List _selectedOptions = []; + bool _isSnackBarVisible = false; // Track SnackBar visibility + + @override + void initState() { + super.initState(); + _selectedOptions = List.from(widget.preSelectedOptions); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + '${widget.title}', + style: const TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16,), + Text( + '${widget.description}', + ), + const SizedBox(height: 16,), + Wrap( + spacing: 8.0, + children: List.generate(widget.options.length, (index) { + final option = widget.options[index]; + return ChoiceChip( + label: Text(_getDisplayText(option)), + selected: _selectedOptions.contains(option), + onSelected: (selected) { + setState(() { + if (selected) { + if (_selectedOptions.length < widget.maxSelections) { + _selectedOptions.add(option); + } else { + // Provide feedback that maximum selections reached + _showSnackBar( + 'Maximum selections reached for ${widget.title}'); + } + } else { + _selectedOptions.remove(option); + } + }); + }, + ); + }), + ), + const SizedBox(height: 16.0), + if (!widget.hideSaveButton) + ElevatedButton( + onPressed: () { + if (!equalContent( + _selectedOptions, widget.preSelectedOptions)) { + // Only save if options have changed + if (_selectedOptions.length >= widget.minSelections) { + widget.onSave(_selectedOptions); + // TODO Navigation abhängig von caller + // Navigator.pop(context); // Navigate back after saving + } else { + // Provide feedback that minimum selections not met + _showSnackBar('No selection made for ${widget.title}'); + } + } else { + print('No changes to save for ${widget.title}.'); + } + }, + child: Text(widget.saveButtonText ?? 'Save'), + ), + ], + ), + ), + ); + } + + String _getDisplayText(dynamic option) { + // Check if the option is an enum and has a displayName property + if (option is Enum) { + final dynamicEnum = option as dynamic; + if (dynamicEnum.displayName != null) { + return dynamicEnum.displayName; + } + } + // Fallback to default toString if not an enum + return option.toString().split('.').last; + } + + void _showSnackBar(String message) { + if (!_isSnackBarVisible) { + _isSnackBarVisible = true; + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.red, + //duration: Duration(seconds: 2), + action: SnackBarAction( + label: 'Dismiss', + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + _isSnackBarVisible = false; + }, + ), + ), + ) + .closed + .then((value) { + _isSnackBarVisible = false; + }); + } + } +} diff --git a/lib/forms/skills_form.dart b/lib/forms/skills_form.dart new file mode 100644 index 0000000..77c4b7d --- /dev/null +++ b/lib/forms/skills_form.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import '../../constants.dart'; +import '../../enumerations.dart'; +import '../../services/auth/auth_service.dart'; +import 'profile_category_form.dart'; + +class SkillsForm extends StatelessWidget { + SkillsForm({super.key, required this.skillsSought}); + + // get instance of firestore and auth + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final AuthService _authService = AuthService(); + + final bool skillsSought; // flag to toggle offered and sought skills + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: getSkillsFromFirebase(), // Fetch skills from Firebase + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + // Show loading indicator while fetching data + return const CircularProgressIndicator(); + } else if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else { + List? userSkills = snapshot.data; + return ProfileCategoryForm( + title: 'Skills', + description: skillsSought + ? 'Choose up to 3 areas you are looking for in a cofounder' + : 'Select up to 3 areas in which you are skilled', + options: SkillOption.values.toList(), // Convert enum values to list + minSelections: 1, + maxSelections: 3, + preSelectedOptions: + userSkills ?? [], // Pass pre-selected skills to the form + onSave: (selectedOptions) { + // Handle saving selected options + saveSkillsToFirebase(selectedOptions.cast()); + // Then navigate to another screen or perform any other action??? + }, + ); + } + }, + ); + } + + Future> getSkillsFromFirebase() async { + // Fetch skills from Firestore + String currentUserId = _authService.getCurrentUser()!.uid; + DocumentSnapshot userDoc = await _firestore + .collection(Constants.dbCollectionUsers) + .doc(currentUserId) + .get(); + + if (userDoc.exists && userDoc.data() != null) { + Map userData = + userDoc.data()! as Map; // Explicit cast + + List? skills; + if (skillsSought) { + skills = userData[Constants.dbFieldUsersSkillsSought]; + } else { + skills = userData[ + Constants.dbFieldUsersSkills]; //as List?; // Explicit cast + } + + if (skills != null && skills.isNotEmpty) { + // Convert skills from strings to enum values + List userSkills = skills + .map((skill) => SkillOption.values + .firstWhere((x) => x.toString() == 'SkillOption.$skill')) + .toList(); + return userSkills; + } + } + + return []; + } + + void saveSkillsToFirebase(List selectedOptions) { + String currentUserId = _authService.getCurrentUser()!.uid; + + // Convert enum values to strings, removing leading EnumType with split + List skills = selectedOptions + .map((option) => option.toString().split('.').last) + .toList(); + + // Update the corresponding 'skills' field in the user's document + String keyToUpdate = skillsSought + ? Constants.dbFieldUsersSkillsSought + : Constants.dbFieldUsersSkills; + + _firestore + .collection(Constants.dbCollectionUsers) + .doc(currentUserId) + .update({ + keyToUpdate: skills, + }).then((_) { + print('$keyToUpdate saved to Firebase: $skills'); + }).catchError((error) { + print('Failed to save $keyToUpdate: $error'); + }); + } +} diff --git a/lib/helper.dart b/lib/helper.dart index b860ff2..e3d74de 100644 --- a/lib/helper.dart +++ b/lib/helper.dart @@ -1,4 +1,15 @@ -/// Convert decimal coordinate to degrees minutes seconds (DMS) +import 'package:collection/collection.dart'; + +/// +/// Compare two lists by their content ignoring their elements order. +/// +bool equalContent(List list1, List list2) { + return const DeepCollectionEquality.unordered().equals(list1, list2); +} + +/// +/// Convert decimal coordinate to degrees minutes seconds (DMS). +/// String convertDecimalToDMS(double decimalValue) { bool isNegative = decimalValue < 0; double absoluteValue = decimalValue.abs(); @@ -15,4 +26,4 @@ String convertDecimalToDMS(double decimalValue) { // return formatted string return '${degrees.abs()}° ${minutes.abs()}\' ${seconds.abs()}" $direction'; -} \ No newline at end of file +} diff --git a/lib/main.dart b/lib/main.dart index d3c4d0a..f00f363 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,6 @@ -import 'package:cofounderella/constants.dart'; -import 'package:cofounderella/services/auth/auth_gate.dart'; -import 'package:cofounderella/themes/theme_provider.dart'; +import '../constants.dart'; +import '../services/auth/auth_gate.dart'; +import '../themes/theme_provider.dart'; import 'package:flutter/material.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:provider/provider.dart'; diff --git a/pubspec.lock b/pubspec.lock index ce3bff6..87489d2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -74,7 +74,7 @@ packages: source: hosted version: "3.12.2" collection: - dependency: transitive + dependency: "direct main" description: name: collection sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a diff --git a/pubspec.yaml b/pubspec.yaml index d6f7e14..8ace6a3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: flutter_svg: ^2.0.10+1 geolocator: ^11.0.0 geocoding: ^3.0.0 + collection: ^1.18.0 dev_dependencies: flutter_test: