623 lines
21 KiB
Dart
623 lines
21 KiB
Dart
import 'dart:convert';
|
|
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_button.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;
|
|
|
|
@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 {
|
|
// Get userID from auth service
|
|
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 != _mainLocationFromDb) {
|
|
if (_mainLocation != null) {
|
|
// Update existing user document with new locations
|
|
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 ? 'User Data' : 'Edit your data'} '),
|
|
centerTitle: true,
|
|
actions: [
|
|
if (widget.isRegProcess)
|
|
IconButton(
|
|
onPressed: () {
|
|
AuthService().signOut();
|
|
},
|
|
icon: const Icon(Icons.logout),
|
|
),
|
|
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 Text(
|
|
'Please fill in the following fields to proceed with the registration process.',
|
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 20),
|
|
const Divider(),
|
|
],
|
|
const SizedBox(height: 10),
|
|
const Text(
|
|
'Location',
|
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
),
|
|
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 Text(
|
|
'Age',
|
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
),
|
|
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 Text(
|
|
'Gender',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
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(),
|
|
Text(
|
|
'Language: (${_selectedLanguages.length} selected)',
|
|
style:
|
|
const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
),
|
|
...languagesList.map(buildSingleCheckbox),
|
|
const Divider(),
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: MyButton(
|
|
text: widget.isRegProcess ? 'Save and continue' : 'Save',
|
|
onTap: () {
|
|
_saveButtonClicked(context);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|