cofounderella/lib/pages/user_data_page.dart

645 lines
22 KiB
Dart

import 'dart:convert' show json;
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../components/location_dialog.dart';
import '../components/my_elevated_button.dart';
import '../components/text_bold.dart';
import '../components/text_with_bold.dart';
import '../constants.dart';
import '../enumerations.dart';
import '../forms/skills_form.dart';
import '../models/language.dart';
import '../models/language_setting.dart';
import '../models/location.dart';
import '../services/auth/auth_service.dart';
import '../utils/helper_dialogs.dart';
import '../utils/math.dart';
class UserDataPage extends StatefulWidget {
final bool isRegProcess;
final bool isEditMode;
const UserDataPage(
{super.key, required this.isRegProcess, required this.isEditMode});
@override
State<UserDataPage> createState() => _UserDataPageState();
}
class _UserDataPageState extends State<UserDataPage> {
MyLocation? _mainLocation;
MyLocation? _secondaryLocation;
List<LanguageSetting> languagesList = [];
final List<Language> _selectedLanguages = [];
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final AuthService _authService = AuthService();
int? _selectedYear;
int? _yearFromDb;
Gender genderView = Gender.none;
int _genderFromDb = 0;
List<Language> _languagesFromDb = [];
MyLocation? _mainLocationFromDb;
MyLocation? _secondaryLocationFromDb;
bool _secondLocationExists = false;
bool _isLanguageListExpanded = false;
static const int _initialLanguageListItemCount = 5;
@override
void initState() {
super.initState();
// load settings from database
_fetchSettings();
}
Future<void> _fetchSettings() async {
try {
String currentUserId = _authService.getCurrentUser()!.uid;
// Fetch user document fields (email, uid, gender, ...) from database
DocumentSnapshot userSnapshot = await _firestore
.collection(Constants.dbCollectionUsers)
.doc(currentUserId)
.get();
if (!userSnapshot.exists || userSnapshot.data() == null) {
return; // should not happen, user entry should exist
}
Map<String, dynamic> userFields =
userSnapshot.data() as Map<String, dynamic>;
// Extract gender and birth year
if (userFields.containsKey(Constants.dbFieldUsersGender)) {
_genderFromDb = userSnapshot[Constants.dbFieldUsersGender];
}
if (userFields.containsKey(Constants.dbFieldUsersYearBorn)) {
_yearFromDb = userSnapshot[Constants.dbFieldUsersYearBorn];
}
// Fetch locations
QuerySnapshot locationSnapshot = await _firestore
.collection(Constants.dbCollectionUsers)
.doc(currentUserId)
.collection(Constants.dbCollectionLocations)
.get();
MyLocation userLoc;
for (var doc in locationSnapshot.docs) {
userLoc = MyLocation(
street: doc[Constants.dbFieldLocationStreet],
country: doc[Constants.dbFieldLocationCountry],
administrativeArea: doc[Constants.dbFieldLocationArea],
locality: doc[Constants.dbFieldLocationLocality],
subLocality: doc[Constants.dbFieldLocationSubLocality],
postalCode: doc[Constants.dbFieldLocationPostalCode],
latitude: doc[Constants.dbFieldLocationLatitude],
longitude: doc[Constants.dbFieldLocationLongitude],
);
if (doc.id == Constants.dbDocMainLocation) {
_mainLocationFromDb = userLoc;
} else if (doc.id == Constants.dbDocSecondLocation) {
_secondaryLocationFromDb = userLoc;
_secondLocationExists = true;
}
}
// Fetch languages
QuerySnapshot languagesSnapshot = await _firestore
.collection(Constants.dbCollectionUsers)
.doc(currentUserId)
.collection(Constants.dbCollectionLanguages)
.get();
List<Language> userLanguages = [];
for (var doc in languagesSnapshot.docs) {
//languages.add(Language.fromDocument(doc));
userLanguages.add(Language(
code: doc.id, // aka doc['code'],
name: doc['name'],
nativeName: doc['nativeName'],
iconFile: doc['iconFile'],
));
}
setState(() {
genderView = Gender.values[_genderFromDb];
_selectedYear = _yearFromDb;
_languagesFromDb = userLanguages;
_mainLocation = _mainLocationFromDb;
_secondaryLocation = _secondaryLocationFromDb;
});
// Load data from JSON file when the widget initializes
loadLanguagesFromJson();
} catch (error) {
_showSnackBar('Error fetching settings: $error');
}
}
/// Loads the languages to display
Future<void> loadLanguagesFromJson() async {
// Load JSON data from assets
String jsonData = await rootBundle.loadString(Constants.pathLanguagesJson);
// Parse JSON into a list of objects
List<dynamic> jsonList = await json.decode(jsonData);
// Create LanguageSetting objects from JSON data
languagesList = jsonList.map((item) {
Language language = Language(
code: item['code'],
name: item['name'],
nativeName: item['nativeName'],
iconFile: item['iconFile'],
);
return LanguageSetting(
language: language,
isSelected: _languagesFromDb.any(
(language) => language.code == item['code'],
),
);
}).toList();
// Update the UI after loading data
setState(() {
if (_selectedLanguages.isEmpty) {
_selectedLanguages.addAll(_languagesFromDb);
}
});
}
Future<bool> _saveUserData(BuildContext context) async {
try {
String currentUserID = _authService.getCurrentUser()!.uid;
// Get references to the current users Firebase collections
DocumentReference userRef =
_firestore.collection(Constants.dbCollectionUsers).doc(currentUserID);
CollectionReference languagesRef =
userRef.collection(Constants.dbCollectionLanguages);
CollectionReference locationsRef =
userRef.collection(Constants.dbCollectionLocations);
if (_selectedYear != _yearFromDb) {
await userRef.update(
{Constants.dbFieldUsersYearBorn: _selectedYear},
);
// update local value
_yearFromDb = _selectedYear;
}
// Update Gender in database - only if value has changed
if (_genderFromDb != genderView.index) {
await userRef.update(
{Constants.dbFieldUsersGender: genderView.index},
);
// update local value
_genderFromDb = genderView.index;
}
// Save locations
if (_mainLocation == null || _mainLocation != _mainLocationFromDb) {
if (_mainLocation != null) {
await locationsRef
.doc(Constants.dbDocMainLocation)
.set(_mainLocation!.toMap());
} else {
_showSnackBar('No location selected');
return false;
}
}
if (_secondaryLocation != _secondaryLocationFromDb) {
if (_secondaryLocation != null) {
await locationsRef
.doc(Constants.dbDocSecondLocation)
.set(_secondaryLocation!.toMap())
.then((value) => {
// update local values
_secondaryLocationFromDb = _secondaryLocation,
_secondLocationExists = true
});
} else if (_secondLocationExists) {
// secondLocationExists but is null here -> delete secondary in DB
await locationsRef.doc(Constants.dbDocSecondLocation).delete().then(
(doc) => {
// update local values
_secondaryLocationFromDb = null,
_secondLocationExists = false
},
onError: (e) => _showSnackBar('Error updating document: $e'),
);
}
}
// Save Languages - only if selected values changed
if (_selectedLanguages.isEmpty) {
_showSnackBar('No language selected');
return false;
} else if (_languagesFromDb.length != _selectedLanguages.length ||
_selectedLanguages
.any((element) => !_languagesFromDb.contains(element))) {
// Loop through each Language object and save it to the collection
for (int i = 0; i < _selectedLanguages.length; i++) {
// Convert Language object to a map and add the map to the collection,
// using .doc(myID).set() instead of .add() to avoid duplicates.
await languagesRef
.doc(_selectedLanguages[i].code)
.set(_selectedLanguages[i].toMap());
}
// update local variable (only if selection is not empty)
_languagesFromDb.clear();
_languagesFromDb.addAll(_selectedLanguages);
// List to store language codes from the provided list
List<String> languageCodes =
_selectedLanguages.map((language) => language.code).toList();
// Clean up languages that were not part of the provided list
await _deleteUnusedDocuments(languagesRef, languageCodes);
}
return true;
} catch (e) {
_showSnackBar(e.toString());
return false;
}
}
/// Deletes documents from collection that are not part of the provided list
Future<void> _deleteUnusedDocuments(
CollectionReference refCollection, List<String> idsToKeep) async {
// Fetch the existing documents from the database
QuerySnapshot snapshot = await refCollection.get();
// Loop through each document in the collection
for (QueryDocumentSnapshot doc in snapshot.docs) {
String documentId = doc.id;
// If the language code is not in the provided list, delete the document
if (!idsToKeep.contains(documentId)) {
await doc.reference.delete();
}
}
}
void _updateSelectedGender(Set<Gender> newSelection) {
setState(() {
// By default there is only a single segment that can be
// selected at one time, so its value is always the first
// item in the selected set.
genderView = newSelection.first;
});
}
// Function to show location dialog
void _showLocationDialog(bool isPrimary) {
showDialog(
context: context,
builder: (BuildContext context) {
return LocationDialog(
onLocationSelected: (location) {
setState(() {
if (isPrimary) {
_mainLocation = location;
} else {
_secondaryLocation = location;
}
});
},
);
},
);
}
/// Method to remove secondary location
void _removeSecondaryLocation() {
setState(() {
_secondaryLocation = null;
});
}
void _saveButtonClicked(BuildContext context) async {
bool success = await _saveUserData(context);
if (context.mounted) {
if (success) {
if (widget.isRegProcess) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SkillsForm(
isRegProcess: widget.isRegProcess,
skillsSought: false,
isEditMode: false,
),
),
);
} else {
if (widget.isEditMode == true) {
// pass data back to caller
Map<String, MyLocation?> locations = {
if (_mainLocation != null)
Constants.dbDocMainLocation: _mainLocation,
if (_secondaryLocation != null)
Constants.dbDocSecondLocation: _secondaryLocation,
};
Navigator.pop(context, {
Constants.dbFieldUsersYearBorn: _selectedYear,
Constants.dbFieldUsersGender: genderView,
Constants.dbCollectionLanguages: _languagesFromDb,
Constants.dbCollectionLocations: locations,
});
} else {
Navigator.pop(context);
}
}
} else {
_showSnackBar('Failed to save user data.');
}
}
}
@override
Widget build(BuildContext context) {
if (languagesList.isEmpty) {
return const Center(child: CircularProgressIndicator());
} else {
return Scaffold(
appBar: AppBar(
title: Text(
'${widget.isRegProcess ? 'Personal Details' : 'Edit your details'} '),
centerTitle: true,
actions: [
if (widget.isEditMode && !widget.isRegProcess)
IconButton(
onPressed: () {
_saveButtonClicked(context);
},
icon: const Icon(Icons.save),
)
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: ListView(
children: [
if (widget.isRegProcess) ...[
const SizedBox(height: 10),
const TextBold(
text:
'Please fill in the following fields to proceed with the registration process.',
),
const SizedBox(height: 20),
const Divider(),
],
const SizedBox(height: 10),
const TextBold(text: 'Location'),
const SizedBox(height: 20),
// Display selected main location
const Text(
'Main Location:',
style: TextStyle(fontWeight: FontWeight.bold),
),
if (_mainLocation != null) ...[
Text(_mainLocation!.toString()),
const SizedBox(height: 10),
] else ...[
const Text('n/a'),
const SizedBox(height: 10),
],
// Button to set main location
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Center(
child: ElevatedButton.icon(
icon: _mainLocation != null
? const Icon(Icons.edit_location_alt_outlined)
: const Icon(Icons.location_on_outlined),
label: Text(_mainLocation != null
? 'Edit Main Location'
: 'Set Main Location'),
onPressed: () {
_showLocationDialog(true);
},
),
),
],
),
const SizedBox(height: 20),
// Display selected secondary location
if (_secondaryLocation != null) ...[
const Text(
'Secondary Location:',
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(_secondaryLocation!.toString()),
const SizedBox(height: 10),
],
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
if (_mainLocation != null) ...[
// Button to set secondary location
Center(
child: ElevatedButton.icon(
icon: _secondaryLocation != null
? const Icon(Icons.edit_location_alt_outlined)
: const Icon(Icons.add_location),
onPressed: () {
_showLocationDialog(false);
},
label: Text(_secondaryLocation != null
? 'Edit'
: 'Add location'),
),
),
],
const SizedBox(width: 10),
// Display selected secondary location or remove button
if (_secondaryLocation != null) ...[
const SizedBox(height: 12),
Center(
child: ElevatedButton.icon(
onPressed: _removeSecondaryLocation,
label: const Text('Remove'),
icon: const Icon(Icons.wrong_location_outlined),
),
),
],
],
),
const SizedBox(height: 20),
const TextWithBold(
boldText: 'Age ',
trailingText: ' (optional)',
boldSize: 18,
trailingSize: 12,
),
Row(
children: [
const Padding(padding: EdgeInsets.symmetric(horizontal: 8)),
Text(_selectedYear != null
? '${calcAge(_selectedYear)} years old'
: 'not specified'),
const SizedBox(width: 20),
DropdownMenu(
initialSelection: _selectedYear,
onSelected: (int? newValue) {
setState(() {
_selectedYear = newValue;
});
},
dropdownMenuEntries: List.generate(50, (index) {
return DropdownMenuEntry<int>(
value: DateTime.now().year - 16 - index,
label: '${DateTime.now().year - 16 - index}',
);
}),
label: const Text('birth year'),
),
],
),
const SizedBox(height: 20),
const TextWithBold(
boldText: 'Gender ',
trailingText: ' (optional)',
boldSize: 18,
trailingSize: 12,
),
Align(
alignment: Alignment.centerLeft,
child: SegmentedButton<Gender>(
style: SegmentedButton.styleFrom(
selectedBackgroundColor: Colors.blue,
),
segments: const <ButtonSegment<Gender>>[
ButtonSegment<Gender>(
value: Gender.none,
label: Text('none'),
),
ButtonSegment<Gender>(
value: Gender.male,
label: Text('male'),
),
ButtonSegment<Gender>(
value: Gender.female,
label: Text('female'),
),
ButtonSegment<Gender>(
value: Gender.divers,
label: Text('diverse'),
),
],
selected: <Gender>{genderView},
showSelectedIcon: false,
onSelectionChanged: _updateSelectedGender,
),
),
const SizedBox(height: 20),
const Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextBold(
text: 'Language: (${_selectedLanguages.length} selected)',
),
_buildExpandCollapseButton(),
],
),
..._buildLanguageList(),
const Divider(),
Padding(
padding: const EdgeInsets.all(8.0),
child: MyElevatedButton(
child: Text(
widget.isRegProcess ? 'Save and continue' : 'Save',
),
onPressed: () {
_saveButtonClicked(context);
},
),
),
],
),
),
);
}
}
List<Widget> _buildLanguageList() {
List<LanguageSetting> displayedLanguages = _isLanguageListExpanded
? languagesList
: languagesList.take(_initialLanguageListItemCount).toList();
return displayedLanguages.map(buildSingleCheckbox).toList();
}
Widget _buildExpandCollapseButton() {
if (languagesList.length <= _initialLanguageListItemCount) {
return Container();
}
return TextButton(
onPressed: () {
setState(() {
_isLanguageListExpanded = !_isLanguageListExpanded;
});
},
child: Text(
_isLanguageListExpanded ? 'Show less' : 'Show full list',
),
);
}
Widget buildSingleCheckbox(LanguageSetting languageSetting) => buildCheckbox(
languageSetting: languageSetting,
onClicked: () {
setState(() {
final newValue = !languageSetting.isSelected;
languageSetting.isSelected = newValue;
if (languageSetting.isSelected &&
!_selectedLanguages.contains(languageSetting.language)) {
_selectedLanguages.add(languageSetting.language);
} else if (languageSetting.isSelected == false) {
_selectedLanguages.removeWhere(
(element) => element.code == languageSetting.language.code);
}
});
},
);
Widget buildCheckbox({
required LanguageSetting languageSetting,
required VoidCallback onClicked,
}) =>
ListTile(
selected: languageSetting.isSelected,
selectedColor: Colors.blue,
onTap: onClicked,
leading: SizedBox(
width: 48,
height: 32,
child: SvgPicture.asset(
languageSetting.language.iconFile,
),
),
title: TextWithBold(
boldText: languageSetting.language.nativeName,
trailingText: ' / ${languageSetting.language.name}',
),
trailing: Checkbox(
value: languageSetting.isSelected,
onChanged: (value) => onClicked(),
),
);
void _showSnackBar(String message) {
showErrorSnackBar(context, message);
}
}