diff --git a/lib/screens/profile_screen.dart b/lib/screens/profile_screen.dart index fe90ab1..bd84a0e 100644 --- a/lib/screens/profile_screen.dart +++ b/lib/screens/profile_screen.dart @@ -46,6 +46,13 @@ class _ProfileScreenState extends State String? _userTier; String? _district; DateTime? _districtChangedAt; + String? _firstName; + String? _lastName; + String? _phone; + String? _place; + String? _pincode; + String? _state; + String? _country; final ImagePicker _picker = ImagePicker(); // Share rank @@ -206,6 +213,15 @@ class _ProfileScreenState extends State _districtChangedAt = DateTime.tryParse(districtChangedStr); } + // Personal info fields + _firstName = prefs.getString('first_name'); + _lastName = prefs.getString('last_name'); + _phone = prefs.getString('phone_number'); + _place = prefs.getString('place'); + _pincode = prefs.getString('pincode'); + _state = prefs.getString('state'); + _country = prefs.getString('country'); + await _loadEventsForProfile(prefs); if (mounted) setState(() {}); } @@ -241,10 +257,31 @@ class _ProfileScreenState extends State if (changedAtStr.isNotEmpty) { await prefs.setString('district_changed_at', changedAtStr); } + // Personal info fields from status response + final fields = { + 'first_name': res['first_name']?.toString() ?? '', + 'last_name': res['last_name']?.toString() ?? '', + 'phone_number': res['phone_number']?.toString() ?? '', + 'place': res['place']?.toString() ?? '', + 'pincode': res['pincode']?.toString() ?? '', + 'state': res['state']?.toString() ?? '', + 'country': res['country']?.toString() ?? '', + }; + for (final e in fields.entries) { + if (e.value.isNotEmpty) await prefs.setString(e.key, e.value); + } + if (mounted) { setState(() { if (districtFromServer.isNotEmpty) _district = districtFromServer; if (changedAtStr.isNotEmpty) _districtChangedAt = DateTime.tryParse(changedAtStr); + if (fields['first_name']!.isNotEmpty) _firstName = fields['first_name']; + if (fields['last_name']!.isNotEmpty) _lastName = fields['last_name']; + if (fields['phone_number']!.isNotEmpty) _phone = fields['phone_number']; + if (fields['place']!.isNotEmpty) _place = fields['place']; + if (fields['pincode']!.isNotEmpty) _pincode = fields['pincode']; + if (fields['state']!.isNotEmpty) _state = fields['state']; + if (fields['country']!.isNotEmpty) _country = fields['country']; }); } } catch (_) { @@ -658,119 +695,130 @@ class _ProfileScreenState extends State } Future _openEditDialog() async { - final nameCtl = TextEditingController(text: _username); - final emailCtl = TextEditingController(text: _email); + final firstNameCtl = TextEditingController(text: _firstName ?? ''); + final lastNameCtl = TextEditingController(text: _lastName ?? ''); + final emailCtl = TextEditingController(text: _email == 'not provided' ? '' : _email); + final phoneCtl = TextEditingController(text: _phone ?? ''); + final placeCtl = TextEditingController(text: _place ?? ''); + final pincodeCtl = TextEditingController(text: _pincode ?? ''); + final stateCtl = TextEditingController(text: _state ?? ''); + final countryCtl = TextEditingController(text: _country ?? ''); + final gamDistrict = context.read().profile?.district; showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (ctx) { - // nameCtl and emailCtl are disposed via .then() below final theme = Theme.of(ctx); + final inputDecoration = InputDecoration( + filled: true, + fillColor: theme.cardColor, + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + ); + return DraggableScrollableSheet( expand: false, - initialChildSize: 0.44, - minChildSize: 0.28, - maxChildSize: 0.92, + initialChildSize: 0.85, + minChildSize: 0.5, + maxChildSize: 0.95, builder: (context, scrollController) { return Container( - padding: const EdgeInsets.fromLTRB(18, 14, 18, 18), + padding: const EdgeInsets.fromLTRB(18, 14, 18, 32), decoration: BoxDecoration( color: theme.cardColor, - borderRadius: - const BorderRadius.vertical(top: Radius.circular(20)), + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: SingleChildScrollView( controller: scrollController, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - width: 48, - height: 6, + // Handle bar + Center( + child: Container( + width: 48, height: 6, decoration: BoxDecoration( - color: theme.dividerColor, - borderRadius: BorderRadius.circular(6))), + color: theme.dividerColor, + borderRadius: BorderRadius.circular(6), + ), + ), + ), const SizedBox(height: 12), + + // Header row Row( children: [ - Text('Edit profile', + Text('Personal Info', style: theme.textTheme.titleMedium ?.copyWith(fontWeight: FontWeight.bold)), const Spacer(), IconButton( icon: const Icon(Icons.photo_camera), + tooltip: 'Change photo', onPressed: () async { Navigator.of(context).pop(); await _pickFromGallery(); }, ), - IconButton( - icon: const Icon(Icons.link), - onPressed: () { - Navigator.of(context).pop(); - _enterAssetPathDialog(); - }, - ), ], ), - const SizedBox(height: 8), - TextField( - controller: nameCtl, - decoration: InputDecoration( - labelText: 'Name', - filled: true, - fillColor: theme.cardColor, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8)))), - const SizedBox(height: 8), - TextField( - controller: emailCtl, - decoration: InputDecoration( - labelText: 'Email', - filled: true, - fillColor: theme.cardColor, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8)))), - const SizedBox(height: 14), + const SizedBox(height: 16), + + // ── Personal info ──────────────────────────────────── Row( children: [ - const Spacer(), - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel')), - const SizedBox(width: 8), - ElevatedButton( - onPressed: () async { - final newName = nameCtl.text.trim().isEmpty - ? _email - : nameCtl.text.trim(); - final newEmail = emailCtl.text.trim().isEmpty - ? _email - : emailCtl.text.trim(); - await _saveProfile(newName, newEmail, _profileImage); - Navigator.of(context).pop(); - }, - child: const Text('Save'), + Expanded( + child: TextField( + controller: firstNameCtl, + textCapitalization: TextCapitalization.words, + decoration: inputDecoration.copyWith(labelText: 'First Name *'), + ), + ), + const SizedBox(width: 10), + Expanded( + child: TextField( + controller: lastNameCtl, + textCapitalization: TextCapitalization.words, + decoration: inputDecoration.copyWith(labelText: 'Last Name'), + ), ), ], ), - const SizedBox(height: 8), - Text( - 'Tip: tap the camera icon to pick from gallery (mobile). Or tap the link icon to paste an asset path or URL.', - style: theme.textTheme.bodySmall - ?.copyWith(color: theme.hintColor), + const SizedBox(height: 10), + + TextField( + controller: emailCtl, + keyboardType: TextInputType.emailAddress, + decoration: inputDecoration.copyWith(labelText: 'Email *'), + ), + const SizedBox(height: 10), + + TextField( + controller: phoneCtl, + keyboardType: TextInputType.phone, + decoration: inputDecoration.copyWith(labelText: 'Phone'), ), const SizedBox(height: 20), - // PROF-002: District — tap to open dedicated picker sheet + // ── Location ───────────────────────────────────────── + Text('Location', + style: theme.textTheme.labelLarge?.copyWith( + color: theme.hintColor, + fontWeight: FontWeight.w600, + letterSpacing: 0.8, + )), + const SizedBox(height: 10), + + // District — tap to open picker (locked with cooldown) GestureDetector( onTap: () { Navigator.of(context).pop(); _showDistrictPicker(); }, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), decoration: BoxDecoration( color: theme.cardColor, borderRadius: BorderRadius.circular(8), @@ -778,12 +826,27 @@ class _ProfileScreenState extends State ), child: Row( children: [ - const Icon(Icons.location_on_outlined, size: 18), - const SizedBox(width: 8), Expanded( - child: Text( - _district ?? 'Tap to set district', - style: theme.textTheme.bodyMedium, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Home District', + style: theme.textTheme.bodySmall + ?.copyWith(color: theme.hintColor)), + const SizedBox(height: 2), + Text( + gamDistrict ?? _district ?? 'Tap to set district', + style: theme.textTheme.bodyMedium, + ), + if (_districtLocked) ...[ + const SizedBox(height: 2), + Text( + 'Next change: $_districtNextChange', + style: theme.textTheme.bodySmall + ?.copyWith(color: Colors.amber[700]), + ), + ], + ], ), ), Icon( @@ -795,7 +858,122 @@ class _ProfileScreenState extends State ), ), ), - const SizedBox(height: 8), + const SizedBox(height: 10), + + TextField( + controller: placeCtl, + textCapitalization: TextCapitalization.words, + decoration: inputDecoration.copyWith(labelText: 'Place'), + ), + const SizedBox(height: 10), + + TextField( + controller: pincodeCtl, + keyboardType: TextInputType.number, + decoration: inputDecoration.copyWith(labelText: 'Pincode'), + ), + const SizedBox(height: 10), + + TextField( + controller: stateCtl, + textCapitalization: TextCapitalization.words, + decoration: inputDecoration.copyWith(labelText: 'State'), + ), + const SizedBox(height: 10), + + TextField( + controller: countryCtl, + textCapitalization: TextCapitalization.words, + decoration: inputDecoration.copyWith(labelText: 'Country'), + ), + const SizedBox(height: 20), + + // ── Save / Cancel ───────────────────────────────────── + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: () async { + final firstName = firstNameCtl.text.trim(); + final lastName = lastNameCtl.text.trim(); + final email = emailCtl.text.trim(); + final phone = phoneCtl.text.trim(); + final place = placeCtl.text.trim(); + final pincode = pincodeCtl.text.trim(); + final stateVal = stateCtl.text.trim(); + final country = countryCtl.text.trim(); + + Navigator.of(context).pop(); + + try { + // Build display name from first + last + final displayName = [firstName, lastName] + .where((s) => s.isNotEmpty) + .join(' '); + + // API call + await _apiClient.post( + ApiEndpoints.updateProfile, + body: { + if (firstName.isNotEmpty) 'first_name': firstName, + if (lastName.isNotEmpty) 'last_name': lastName, + if (email.isNotEmpty) 'email': email, + if (phone.isNotEmpty) 'phone_number': phone, + if (place.isNotEmpty) 'place': place, + if (pincode.isNotEmpty) 'pincode': pincode, + if (stateVal.isNotEmpty) 'state': stateVal, + if (country.isNotEmpty) 'country': country, + }, + ); + + // Persist to prefs + final prefs = await SharedPreferences.getInstance(); + if (firstName.isNotEmpty) await prefs.setString('first_name', firstName); + if (lastName.isNotEmpty) await prefs.setString('last_name', lastName); + if (phone.isNotEmpty) await prefs.setString('phone_number', phone); + if (place.isNotEmpty) await prefs.setString('place', place); + if (pincode.isNotEmpty) await prefs.setString('pincode', pincode); + if (stateVal.isNotEmpty) await prefs.setString('state', stateVal); + if (country.isNotEmpty) await prefs.setString('country', country); + + // Update display name if first name provided + if (displayName.isNotEmpty) { + await _saveProfile(displayName, email.isNotEmpty ? email : _email, _profileImage); + } else if (email.isNotEmpty) { + await _saveProfile(_username, email, _profileImage); + } + + if (mounted) { + setState(() { + if (firstName.isNotEmpty) _firstName = firstName; + if (lastName.isNotEmpty) _lastName = lastName; + if (phone.isNotEmpty) _phone = phone; + if (place.isNotEmpty) _place = place; + if (pincode.isNotEmpty) _pincode = pincode; + if (stateVal.isNotEmpty) _state = stateVal; + if (country.isNotEmpty) _country = country; + }); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Profile updated')), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(userFriendlyError(e))), + ); + } + } + }, + child: const Text('Save'), + ), + ], + ), ], ), ), @@ -804,8 +982,14 @@ class _ProfileScreenState extends State ); }, ).then((_) { - nameCtl.dispose(); + firstNameCtl.dispose(); + lastNameCtl.dispose(); emailCtl.dispose(); + phoneCtl.dispose(); + placeCtl.dispose(); + pincodeCtl.dispose(); + stateCtl.dispose(); + countryCtl.dispose(); }); } @@ -1084,7 +1268,7 @@ class _ProfileScreenState extends State const Icon(Icons.location_on_outlined, color: Colors.white, size: 18), const SizedBox(width: 4), Text( - _district ?? 'Tap to set district', + p?.district ?? _district ?? 'Tap to set district', style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w400), ), const SizedBox(width: 4),