import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import '../models/school.dart'; import '../services/vanguard_api.dart'; import '../widgets/yogibook_background.dart'; import '../widgets/app_bottom_nav.dart'; class AccountPage extends StatefulWidget { final String token; final School school; final String? userFirstName; const AccountPage({ super.key, required this.token, required this.school, this.userFirstName, }); @override State createState() => _AccountPageState(); } class _AccountPageState extends State { static const Color kGreen = Color(0xFF10B981); final _firstNameCtrl = TextEditingController(); final _lastNameCtrl = TextEditingController(); final _passwordCtrl = TextEditingController(); final _passwordConfirmCtrl = TextEditingController(); bool _loadingProfile = true; bool _savingProfile = false; bool _savingPassword = false; bool _uploadingAvatar = false; String _error = ''; String? _avatarUrl; bool _obscurePw = true; bool _obscurePw2 = true; final _picker = ImagePicker(); @override void initState() { super.initState(); _loadProfile(); } @override void dispose() { _firstNameCtrl.dispose(); _lastNameCtrl.dispose(); _passwordCtrl.dispose(); _passwordConfirmCtrl.dispose(); super.dispose(); } String get _firstName => _firstNameCtrl.text.trim(); String get _avatarLetter => _firstName.isNotEmpty ? _firstName[0].toUpperCase() : 'U'; Future _loadProfile() async { setState(() { _loadingProfile = true; _error = ''; }); try { final me = await VanguardApi.getMe(token: widget.token); _firstNameCtrl.text = (me['first_name'] ?? '').toString(); _lastNameCtrl.text = (me['last_name'] ?? '').toString(); final av = (me['avatar'] ?? '').toString().trim(); _avatarUrl = av.isEmpty ? null : av; } catch (e) { _error = 'Impossibile caricare il profilo: $e'; } finally { if (mounted) setState(() => _loadingProfile = false); } } void _snack(String msg) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); } // ---------- Salva nome/cognome ---------- Future _saveProfile() async { if (_firstName.isEmpty || _lastNameCtrl.text.trim().isEmpty) { _snack('Nome e cognome non possono essere vuoti.'); return; } setState(() => _savingProfile = true); try { await VanguardApi.updateDetails( token: widget.token, firstName: _firstName, lastName: _lastNameCtrl.text.trim(), ); _snack('Profilo aggiornato.'); if (mounted) FocusScope.of(context).unfocus(); } catch (e) { _snack('Errore aggiornamento profilo: $e'); } finally { if (mounted) setState(() => _savingProfile = false); } } // ---------- Cambia password ---------- Future _savePassword() async { final pw = _passwordCtrl.text; final pw2 = _passwordConfirmCtrl.text; if (pw.isEmpty || pw2.isEmpty) { _snack('Compila entrambi i campi password.'); return; } if (pw.length < 6) { _snack('La password deve avere almeno 6 caratteri.'); return; } if (pw != pw2) { _snack('Le due password non coincidono.'); return; } setState(() => _savingPassword = true); try { await VanguardApi.updatePassword( token: widget.token, password: pw, passwordConfirmation: pw2, ); _passwordCtrl.clear(); _passwordConfirmCtrl.clear(); if (mounted) FocusScope.of(context).unfocus(); _snack('Password aggiornata.'); } catch (e) { _snack('Errore cambio password: $e'); } finally { if (mounted) setState(() => _savingPassword = false); } } // ---------- Avatar ---------- Future _pickAvatar(ImageSource source) async { try { final XFile? file = await _picker.pickImage( source: source, maxWidth: 1024, maxHeight: 1024, imageQuality: 85, ); if (file == null) return; setState(() => _uploadingAvatar = true); final newUrl = await VanguardApi.uploadAvatar( token: widget.token, filePath: file.path, ); setState(() { // se il server ritorna l'URL usalo, così si aggiorna dalla rete; // in fallback mostriamo il file locale appena scelto _avatarUrl = newUrl ?? _avatarUrl; _localAvatarPath = file.path; }); _snack('Avatar aggiornato.'); } catch (e) { _snack('Errore avatar: $e'); } finally { if (mounted) setState(() => _uploadingAvatar = false); } } String? _localAvatarPath; void _showAvatarSheet() { showModalBottomSheet( context: context, backgroundColor: Colors.white, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (_) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 8), Container( width: 40, height: 4, decoration: BoxDecoration( color: Colors.black26, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 8), ListTile( leading: const Icon(Icons.photo_library_rounded, color: kGreen), title: const Text('Scegli dalla galleria'), onTap: () { Navigator.pop(context); _pickAvatar(ImageSource.gallery); }, ), ListTile( leading: const Icon(Icons.photo_camera_rounded, color: kGreen), title: const Text('Scatta una foto'), onTap: () { Navigator.pop(context); _pickAvatar(ImageSource.camera); }, ), const SizedBox(height: 8), ], ), ), ); } ImageProvider? get _avatarImage { if (_localAvatarPath != null) return FileImage(File(_localAvatarPath!)); if (_avatarUrl != null) return NetworkImage(_avatarUrl!); return null; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, centerTitle: true, title: const Text( 'Account', style: TextStyle(fontWeight: FontWeight.w800, fontSize: 18), ), ), bottomNavigationBar: AppBottomNav( currentIndex: 2, token: widget.token, school: widget.school, userFirstName: widget.userFirstName, ), body: YogibookBackground( child: SafeArea( top: false, child: _loadingProfile ? const Center(child: CircularProgressIndicator()) : _error.isNotEmpty ? _ErrorRetry(message: _error, onRetry: _loadProfile) : ListView( padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), children: [ _avatarBlock(), const SizedBox(height: 20), _profileCard(), const SizedBox(height: 16), _passwordCard(), const SizedBox(height: 8), ], ), ), ), ); } // ---------- UI blocks ---------- Widget _avatarBlock() { return Center( child: Column( children: [ Stack( children: [ Container( width: 116, height: 116, decoration: BoxDecoration( shape: BoxShape.circle, color: const Color(0xFFE7F8F1), boxShadow: const [ BoxShadow( blurRadius: 18, color: Color(0x1A000000), offset: Offset(0, 10), ), ], border: Border.all(color: Colors.white, width: 3), ), child: ClipOval( child: _avatarImage != null ? Image( image: _avatarImage!, width: 116, height: 116, fit: BoxFit.cover, errorBuilder: (_, __, ___) => _avatarFallback(), ) : _avatarFallback(), ), ), Positioned( right: 0, bottom: 0, child: GestureDetector( onTap: _uploadingAvatar ? null : _showAvatarSheet, child: Container( width: 38, height: 38, decoration: BoxDecoration( color: kGreen, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 3), ), child: _uploadingAvatar ? const Padding( padding: EdgeInsets.all(9), child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Icon( Icons.photo_camera_rounded, color: Colors.white, size: 18, ), ), ), ), ], ), const SizedBox(height: 10), Text( _firstName.isEmpty ? 'Il tuo profilo' : '$_firstName ${_lastNameCtrl.text.trim()}', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900), ), ], ), ); } Widget _avatarFallback() { return Container( width: 116, height: 116, alignment: Alignment.center, color: kGreen, child: Text( _avatarLetter, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w900, fontSize: 44, ), ), ); } Widget _card({required String title, required List children}) { return Container( padding: const EdgeInsets.fromLTRB(16, 14, 16, 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(18), boxShadow: const [ BoxShadow( blurRadius: 18, color: Color(0x12000000), offset: Offset(0, 10), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w900), ), const SizedBox(height: 14), ...children, ], ), ); } Widget _profileCard() { return _card( title: 'Profilo', children: [ TextField( controller: _firstNameCtrl, textInputAction: TextInputAction.next, decoration: _inputDecoration('Nome', Icons.person_outline), ), const SizedBox(height: 12), TextField( controller: _lastNameCtrl, textInputAction: TextInputAction.done, decoration: _inputDecoration('Cognome', Icons.badge_outlined), ), const SizedBox(height: 16), _primaryButton( label: 'Salva profilo', loading: _savingProfile, onPressed: _saveProfile, ), ], ); } Widget _passwordCard() { return _card( title: 'Cambia password', children: [ TextField( controller: _passwordCtrl, obscureText: _obscurePw, textInputAction: TextInputAction.next, decoration: _inputDecoration('Nuova password', Icons.lock_outline) .copyWith( suffixIcon: IconButton( icon: Icon( _obscurePw ? Icons.visibility : Icons.visibility_off, ), onPressed: () => setState(() => _obscurePw = !_obscurePw), ), ), ), const SizedBox(height: 12), TextField( controller: _passwordConfirmCtrl, obscureText: _obscurePw2, textInputAction: TextInputAction.done, decoration: _inputDecoration('Conferma password', Icons.lock_outline) .copyWith( suffixIcon: IconButton( icon: Icon( _obscurePw2 ? Icons.visibility : Icons.visibility_off, ), onPressed: () => setState(() => _obscurePw2 = !_obscurePw2), ), ), ), const SizedBox(height: 16), _primaryButton( label: 'Aggiorna password', loading: _savingPassword, onPressed: _savePassword, ), ], ); } InputDecoration _inputDecoration(String label, IconData icon) { return InputDecoration( labelText: label, prefixIcon: Icon(icon, size: 20), filled: true, fillColor: const Color(0xFFF6F6FB), border: OutlineInputBorder( borderRadius: BorderRadius.circular(14), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(14), borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(14), borderSide: const BorderSide(color: kGreen, width: 2), ), ); } Widget _primaryButton({ required String label, required bool loading, required VoidCallback onPressed, }) { return SizedBox( width: double.infinity, height: 48, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: kGreen, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), ), ), onPressed: loading ? null : onPressed, child: loading ? const SizedBox( height: 18, width: 18, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Text( label, style: const TextStyle( fontWeight: FontWeight.w900, color: Colors.white, ), ), ), ); } } class _ErrorRetry extends StatelessWidget { final String message; final VoidCallback onRetry; const _ErrorRetry({required this.message, required this.onRetry}); @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.error_outline, size: 48, color: Colors.black38), const SizedBox(height: 12), Text(message, textAlign: TextAlign.center), const SizedBox(height: 16), ElevatedButton(onPressed: onRetry, child: const Text('Riprova')), ], ), ), ); } }