perbaiki fitur profil
This commit is contained in:
parent
94a8478946
commit
23aa7d659d
|
|
@ -65,6 +65,9 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
if (data['email'] != null) {
|
||||
await prefs.setString('user_email', data['email'] ?? '');
|
||||
}
|
||||
if (data['avatar_url'] != null) {
|
||||
await prefs.setString('user_avatar', data['avatar_url'] ?? '');
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.pushReplacement(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@ import 'package:flutter/material.dart';
|
|||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:geocoding/geocoding.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:convert';
|
||||
import '../utils/api_config.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'dart:io';
|
||||
|
||||
class ProfileScreen extends StatefulWidget {
|
||||
const ProfileScreen({super.key});
|
||||
|
|
@ -18,6 +23,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||
String _userKabupaten = "";
|
||||
String _userKecamatan = "";
|
||||
String _userKelurahan = "";
|
||||
String _userAvatar = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -36,6 +42,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||
_userKabupaten = prefs.getString('user_kabupaten') ?? '';
|
||||
_userKecamatan = prefs.getString('user_kecamatan') ?? '';
|
||||
_userKelurahan = prefs.getString('user_kelurahan') ?? '';
|
||||
_userAvatar = prefs.getString('user_avatar') ?? '';
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -202,10 +209,11 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||
|
||||
void _openEditProfile() {
|
||||
final phoneController = TextEditingController(text: _userPhone);
|
||||
final locationController = TextEditingController(text: _userLocation);
|
||||
final kabupatenController = TextEditingController(text: _userKabupaten);
|
||||
final kecamatanController = TextEditingController(text: _userKecamatan);
|
||||
final kelurahanController = TextEditingController(text: _userKelurahan);
|
||||
final emailController = TextEditingController(text: _userEmail);
|
||||
final passwordController = TextEditingController();
|
||||
bool isPasswordVisible = false;
|
||||
bool isLoading = false;
|
||||
File? pickedImage;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
|
|
@ -214,141 +222,258 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 20,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + 20,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Edit Profil',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setModalState) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 20,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + 20,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: phoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'No. Telepon',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: locationController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Lokasi',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: kabupatenController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Kabupaten',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: kecamatanController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Kecamatan',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: kelurahanController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Kelurahan',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
final address = await _fetchCurrentAddress();
|
||||
if (address == null) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Lokasi tidak terdeteksi'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
locationController.text = address;
|
||||
|
||||
// Load saved values from prefs
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
kabupatenController.text = prefs.getString('user_kabupaten') ?? '';
|
||||
kecamatanController.text = prefs.getString('user_kecamatan') ?? '';
|
||||
kelurahanController.text = prefs.getString('user_kelurahan') ?? '';
|
||||
},
|
||||
icon: const Icon(Icons.my_location),
|
||||
label: const Text('Gunakan lokasi saat ini'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
'user_phone',
|
||||
phoneController.text.trim(),
|
||||
);
|
||||
await prefs.setString(
|
||||
'user_location',
|
||||
locationController.text.trim(),
|
||||
);
|
||||
await prefs.setString(
|
||||
'user_kabupaten',
|
||||
kabupatenController.text.trim(),
|
||||
);
|
||||
await prefs.setString(
|
||||
'user_kecamatan',
|
||||
kecamatanController.text.trim(),
|
||||
);
|
||||
await prefs.setString(
|
||||
'user_kelurahan',
|
||||
kelurahanController.text.trim(),
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_userPhone = phoneController.text.trim();
|
||||
_userLocation = locationController.text.trim();
|
||||
_userKabupaten = kabupatenController.text.trim();
|
||||
_userKecamatan = kecamatanController.text.trim();
|
||||
_userKelurahan = kelurahanController.text.trim();
|
||||
const Text(
|
||||
'Edit Profil',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.grey[200],
|
||||
border: Border.all(color: const Color(0xFF0F703A), width: 2),
|
||||
image: pickedImage != null
|
||||
? DecorationImage(
|
||||
image: FileImage(pickedImage!),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: (_userAvatar.isNotEmpty
|
||||
? DecorationImage(
|
||||
image: NetworkImage(_userAvatar),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: null),
|
||||
),
|
||||
child: pickedImage == null && _userAvatar.isEmpty
|
||||
? const Icon(Icons.person, size: 50, color: Colors.grey)
|
||||
: null,
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
final picker = ImagePicker();
|
||||
final source = await showDialog<ImageSource>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Pilih Sumber Foto'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.camera_alt),
|
||||
title: const Text('Kamera'),
|
||||
onTap: () => Navigator.pop(context, ImageSource.camera),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_library),
|
||||
title: const Text('Galeri'),
|
||||
onTap: () => Navigator.pop(context, ImageSource.gallery),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
if (source != null) {
|
||||
final file = await picker.pickImage(source: source, imageQuality: 80);
|
||||
if (file != null) {
|
||||
setModalState(() {
|
||||
pickedImage = File(file.path);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF0F703A),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.camera_alt, color: Colors.white, size: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextField(
|
||||
controller: phoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'No. Telepon',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: passwordController,
|
||||
obscureText: !isPasswordVisible,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password Baru (Kosongkan jika tidak diubah)',
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
isPasswordVisible ? Icons.visibility : Icons.visibility_off,
|
||||
),
|
||||
onPressed: () {
|
||||
setModalState(() {
|
||||
isPasswordVisible = !isPasswordVisible;
|
||||
});
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0F703A),
|
||||
),
|
||||
child: const Text(
|
||||
'Simpan',
|
||||
style: TextStyle(color: Colors.white),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final navigator = Navigator.of(context);
|
||||
final phone = phoneController.text.trim();
|
||||
final email = emailController.text.trim();
|
||||
final password = passwordController.text;
|
||||
|
||||
if (email.isEmpty) {
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Email tidak boleh kosong'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setModalState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final userId = prefs.getInt('user_id') ?? 0;
|
||||
|
||||
final uri = Uri.parse('${ApiConfig.apiRoot}/mobile/update-profile');
|
||||
final request = http.MultipartRequest('POST', uri);
|
||||
|
||||
request.headers.addAll({
|
||||
'Accept': 'application/json',
|
||||
});
|
||||
|
||||
request.fields['user_id'] = userId.toString();
|
||||
request.fields['email'] = email;
|
||||
if (password.isNotEmpty) {
|
||||
request.fields['password'] = password;
|
||||
}
|
||||
|
||||
if (pickedImage != null) {
|
||||
final multipartFile = await http.MultipartFile.fromPath(
|
||||
'avatar',
|
||||
pickedImage!.path,
|
||||
);
|
||||
request.files.add(multipartFile);
|
||||
}
|
||||
|
||||
final streamedResponse = await request.send();
|
||||
final response = await http.Response.fromStream(streamedResponse);
|
||||
|
||||
final responseData = jsonDecode(response.body);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception(responseData['message'] ?? 'Gagal memperbarui profil');
|
||||
}
|
||||
|
||||
final newAvatarUrl = responseData['data']?['avatar_url'] ?? '';
|
||||
|
||||
// Simpan ke SharedPreferences lokal
|
||||
await prefs.setString('user_phone', phone);
|
||||
await prefs.setString('user_email', email);
|
||||
await prefs.setString('user_avatar', newAvatarUrl);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_userPhone = phone;
|
||||
_userEmail = email;
|
||||
_userAvatar = newAvatarUrl;
|
||||
});
|
||||
}
|
||||
|
||||
navigator.pop();
|
||||
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Profil berhasil diperbarui'),
|
||||
backgroundColor: Color(0xFF0F703A),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Gagal memperbarui profil: ${e.toString().replaceAll('Exception: ', '')}'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
setModalState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0F703A),
|
||||
),
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'Simpan',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -367,19 +492,19 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final navigator = Navigator.of(context);
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('isLoggedIn', false);
|
||||
await prefs.remove('user_id');
|
||||
await prefs.remove('user_name');
|
||||
await prefs.remove('user_email');
|
||||
await prefs.remove('user_avatar');
|
||||
await prefs.remove('user_phone');
|
||||
await prefs.remove('user_location');
|
||||
await prefs.remove('user_kabupaten');
|
||||
await prefs.remove('user_kecamatan');
|
||||
await prefs.remove('user_kelurahan');
|
||||
if (mounted) {
|
||||
Navigator.pushNamedAndRemoveUntil(context, '/', (route) => false);
|
||||
}
|
||||
navigator.pushNamedAndRemoveUntil('/', (route) => false);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||
child: const Text("Logout", style: TextStyle(color: Colors.white)),
|
||||
|
|
@ -430,10 +555,18 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0F703A),
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
image: _userAvatar.isNotEmpty
|
||||
? DecorationImage(
|
||||
image: NetworkImage(_userAvatar),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(Icons.person, size: 100, color: Colors.white),
|
||||
),
|
||||
child: _userAvatar.isEmpty
|
||||
? const Center(
|
||||
child: Icon(Icons.person, size: 100, color: Colors.white),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ public function mobileLogin(Request $request)
|
|||
'user_id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'avatar_url' => $user->avatar ? asset('storage/' . $user->avatar) : null,
|
||||
],
|
||||
], 200);
|
||||
}
|
||||
|
|
@ -120,7 +121,59 @@ public function mobileRegister(Request $request)
|
|||
'user_id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'avatar_url' => null,
|
||||
],
|
||||
], 201);
|
||||
}
|
||||
|
||||
// 6. Mobile Update Profile
|
||||
public function mobileUpdateProfile(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'user_id' => ['required', 'integer', 'exists:users,id'],
|
||||
'email' => ['required', 'email', 'max:255'],
|
||||
'password' => ['nullable', 'string', 'min:4'],
|
||||
'avatar' => ['nullable', 'image', 'mimes:jpeg,png,jpg,gif', 'max:2048'],
|
||||
]);
|
||||
|
||||
$user = User::find($validated['user_id']);
|
||||
|
||||
// Cek jika email sudah digunakan oleh user lain
|
||||
if (User::where('email', $validated['email'])->where('id', '!=', $user->id)->exists()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Email sudah digunakan oleh pengguna lain',
|
||||
], 409);
|
||||
}
|
||||
|
||||
$user->email = $validated['email'];
|
||||
|
||||
if (!empty($validated['password'])) {
|
||||
$user->password = Hash::make($validated['password']);
|
||||
}
|
||||
|
||||
// Proses upload avatar
|
||||
if ($request->hasFile('avatar')) {
|
||||
// Hapus avatar lama jika ada
|
||||
if ($user->avatar) {
|
||||
\Illuminate\Support\Facades\Storage::disk('public')->delete($user->avatar);
|
||||
}
|
||||
// Simpan avatar baru ke public disk di folder 'avatars'
|
||||
$path = $request->file('avatar')->store('avatars', 'public');
|
||||
$user->avatar = $path;
|
||||
}
|
||||
|
||||
$user->save();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Profil berhasil diperbarui',
|
||||
'data' => [
|
||||
'user_id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'avatar_url' => $user->avatar ? asset('storage/' . $user->avatar) : null,
|
||||
],
|
||||
], 200);
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ class User extends Authenticatable
|
|||
'email',
|
||||
'password',
|
||||
'role',
|
||||
'avatar',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('avatar')->nullable()->after('email');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('avatar');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -37,6 +37,7 @@
|
|||
// Mobile auth endpoint
|
||||
Route::post('/mobile/login', [AuthController::class, 'mobileLogin']);
|
||||
Route::post('/mobile/register', [AuthController::class, 'mobileRegister']);
|
||||
Route::post('/mobile/update-profile', [AuthController::class, 'mobileUpdateProfile']);
|
||||
|
||||
// History endpoints
|
||||
Route::prefix('classifications')->group(function () {
|
||||
|
|
|
|||
Loading…
Reference in New Issue