473 lines
15 KiB
Dart
473 lines
15 KiB
Dart
import 'dart:io';
|
|
import 'dart:convert';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import '../pages/login_page.dart';
|
|
import '../bidan/dashboard_bidan.dart';
|
|
|
|
class ProfileBidanPage extends StatefulWidget {
|
|
const ProfileBidanPage({super.key});
|
|
|
|
@override
|
|
State<ProfileBidanPage> createState() => _ProfileBidanPageState();
|
|
}
|
|
|
|
class _ProfileBidanPageState extends State<ProfileBidanPage> {
|
|
final TextEditingController namaC = TextEditingController();
|
|
final TextEditingController emailC = TextEditingController();
|
|
final TextEditingController passwordC = TextEditingController();
|
|
final TextEditingController roleC = TextEditingController();
|
|
final TextEditingController noHpC = TextEditingController();
|
|
final TextEditingController statusC = TextEditingController();
|
|
|
|
String? idUser;
|
|
String? fotoUser;
|
|
XFile? _pickedFile;
|
|
bool _obscurePassword = true;
|
|
bool isEditMode = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadUserData();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
namaC.dispose();
|
|
emailC.dispose();
|
|
passwordC.dispose();
|
|
roleC.dispose();
|
|
noHpC.dispose();
|
|
statusC.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _loadUserData() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final isLogin = prefs.getBool('isLogin') ?? false;
|
|
|
|
if (!isLogin) {
|
|
if (!mounted) return;
|
|
Navigator.pushAndRemoveUntil(
|
|
context,
|
|
MaterialPageRoute(builder: (_) => const LoginPage(fromGuard: true)),
|
|
(route) => false,
|
|
);
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
idUser = prefs.getString('id_user') ?? "";
|
|
namaC.text = prefs.getString('nama') ?? "";
|
|
emailC.text = prefs.getString('email') ?? "";
|
|
passwordC.text = prefs.getString('password') ?? "";
|
|
roleC.text = (prefs.getString('role') ?? "Bidan").toLowerCase();
|
|
String savedNoHp = prefs.getString('no_hp') ?? "";
|
|
noHpC.text = (savedNoHp.isEmpty || savedNoHp == "-") ? "-" : savedNoHp;
|
|
|
|
statusC.text = prefs.getString('status_aktif') ?? "Aktif";
|
|
fotoUser = prefs.getString('foto');
|
|
});
|
|
}
|
|
|
|
// Fungsi Helper untuk SnackBar Peringatan
|
|
void _showEditWarning() {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text("Klik tombol edit di bawah untuk mengubah foto"),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _pickImage() async {
|
|
if (!isEditMode) {
|
|
_showEditWarning();
|
|
return;
|
|
}
|
|
final ImagePicker picker = ImagePicker();
|
|
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
|
|
|
|
if (image != null) {
|
|
setState(() {
|
|
_pickedFile = image;
|
|
});
|
|
}
|
|
}
|
|
|
|
void _removeImage() {
|
|
if (!isEditMode) {
|
|
_showEditWarning();
|
|
return;
|
|
}
|
|
setState(() {
|
|
_pickedFile = null;
|
|
fotoUser = "";
|
|
});
|
|
}
|
|
|
|
void _enableEdit() {
|
|
setState(() {
|
|
isEditMode = true;
|
|
});
|
|
}
|
|
|
|
bool _validatePassword(String value) {
|
|
String pattern = r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{6}$';
|
|
RegExp regExp = RegExp(pattern);
|
|
return regExp.hasMatch(value);
|
|
}
|
|
|
|
bool _validateNoHp(String value) {
|
|
String pattern = r'^[0-9]{10,13}$';
|
|
RegExp regExp = RegExp(pattern);
|
|
return regExp.hasMatch(value);
|
|
}
|
|
|
|
void _saveProfile() async {
|
|
if (!isEditMode) return;
|
|
|
|
if (!_validateNoHp(noHpC.text)) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text("No. HP harus berupa angka dan berjumlah 10-13 digit!"),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!_validatePassword(passwordC.text)) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text("Password harus 6 digit (kombinasi huruf & angka)!"),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
var request = http.MultipartRequest(
|
|
'POST',
|
|
Uri.parse(
|
|
"http://ta.myhost.id/E31230549/mposyandu_api/users/update_profile_bidan.php"),
|
|
);
|
|
|
|
request.fields['id_user'] = idUser ?? "";
|
|
request.fields['nama'] = namaC.text;
|
|
request.fields['email'] = emailC.text;
|
|
request.fields['password'] = passwordC.text;
|
|
request.fields['no_hp'] = noHpC.text;
|
|
request.fields['foto_lama'] = fotoUser ?? "";
|
|
|
|
if (_pickedFile != null) {
|
|
Uint8List data = await _pickedFile!.readAsBytes();
|
|
request.files.add(http.MultipartFile.fromBytes(
|
|
'foto',
|
|
data,
|
|
filename: _pickedFile!.name,
|
|
));
|
|
}
|
|
|
|
var response = await request.send();
|
|
var responseData = await response.stream.bytesToString();
|
|
var result = json.decode(responseData);
|
|
|
|
if (result['status'] == 'success') {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
await prefs.setString('nama', namaC.text);
|
|
await prefs.setString('email', emailC.text);
|
|
await prefs.setString('password', passwordC.text);
|
|
await prefs.setString('no_hp', noHpC.text);
|
|
|
|
if (result['foto'] != null) {
|
|
await prefs.setString('foto', result['foto']);
|
|
setState(() {
|
|
fotoUser = result['foto'];
|
|
});
|
|
}
|
|
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text("Profil berhasil diperbarui")),
|
|
);
|
|
|
|
setState(() {
|
|
isEditMode = false;
|
|
_pickedFile = null;
|
|
});
|
|
} else {
|
|
throw result['message'];
|
|
}
|
|
} catch (e) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text("Gagal menyimpan: $e")),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
bool hasPhoto =
|
|
_pickedFile != null || (fotoUser != null && fotoUser!.isNotEmpty);
|
|
|
|
return PopScope(
|
|
canPop: false,
|
|
onPopInvokedWithResult: (didPop, result) async {
|
|
if (didPop) return;
|
|
// Selalu kembali ke Dashboard Bidan
|
|
Navigator.pushAndRemoveUntil(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => const DashboardBidanPage(),
|
|
),
|
|
(route) => false,
|
|
);
|
|
},
|
|
child: Scaffold(
|
|
backgroundColor: const Color(0xfff4f6fb),
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.blue,
|
|
elevation: 0,
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
|
onPressed: () {
|
|
Navigator.pushAndRemoveUntil(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => const DashboardBidanPage()),
|
|
(route) => false,
|
|
);
|
|
},
|
|
),
|
|
title: Text("",
|
|
style: GoogleFonts.poppins(color: Colors.white, fontSize: 18)),
|
|
),
|
|
body: SingleChildScrollView(
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
width: double.infinity,
|
|
decoration: const BoxDecoration(
|
|
color: Colors.blue,
|
|
borderRadius: BorderRadius.only(
|
|
bottomLeft: Radius.circular(30),
|
|
bottomRight: Radius.circular(30),
|
|
),
|
|
),
|
|
padding: const EdgeInsets.only(bottom: 30),
|
|
child: Column(
|
|
children: [
|
|
Stack(
|
|
children: [
|
|
CircleAvatar(
|
|
radius: 65,
|
|
backgroundColor: Colors.white,
|
|
child: CircleAvatar(
|
|
radius: 60,
|
|
backgroundColor: Colors.blue.shade100,
|
|
backgroundImage: _pickedFile != null
|
|
? (kIsWeb
|
|
? NetworkImage(_pickedFile!.path)
|
|
: FileImage(File(_pickedFile!.path))
|
|
as ImageProvider)
|
|
: (fotoUser != null && fotoUser!.isNotEmpty)
|
|
? NetworkImage(
|
|
"http://ta.myhost.id/E31230549/mposyandu_api/uploads/$fotoUser")
|
|
: null,
|
|
child: !hasPhoto
|
|
? const Icon(Icons.person,
|
|
size: 60, color: Colors.blue)
|
|
: null,
|
|
),
|
|
),
|
|
Positioned(
|
|
bottom: 0,
|
|
right: hasPhoto ? 40 : 0,
|
|
child: GestureDetector(
|
|
onTap: _pickImage,
|
|
child: const CircleAvatar(
|
|
radius: 18,
|
|
backgroundColor: Colors.black,
|
|
child: Icon(Icons.camera_alt,
|
|
color: Colors.white, size: 18),
|
|
),
|
|
),
|
|
),
|
|
if (hasPhoto)
|
|
Positioned(
|
|
bottom: 0,
|
|
right: 0,
|
|
child: GestureDetector(
|
|
onTap: _removeImage,
|
|
child: const CircleAvatar(
|
|
radius: 18,
|
|
backgroundColor: Colors.red,
|
|
child: Icon(Icons.delete,
|
|
color: Colors.white, size: 18),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
namaC.text,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
children: [
|
|
Card(
|
|
color: Colors.white,
|
|
elevation: 2,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(15)),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
children: [
|
|
_buildField("Nama Lengkap", namaC),
|
|
_buildField("Email", emailC),
|
|
_buildPasswordField(
|
|
"Password (6 Digit Huruf & Angka)", passwordC),
|
|
_buildField("Role User", roleC, isReadOnly: true),
|
|
_buildField("No.HP", noHpC, isNumber: true),
|
|
_buildField("Status", statusC, isReadOnly: true),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 30),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildActionButton(
|
|
label: "Edit Profil",
|
|
color: Colors.orange,
|
|
onTap: _enableEdit,
|
|
),
|
|
),
|
|
const SizedBox(width: 15),
|
|
Expanded(
|
|
child: _buildActionButton(
|
|
label: "Simpan",
|
|
color: Colors.blue,
|
|
onTap: _saveProfile,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildField(String label, TextEditingController controller,
|
|
{bool isReadOnly = false, bool isNumber = false}) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 15),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(label,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey[600])),
|
|
const SizedBox(height: 5),
|
|
TextField(
|
|
controller: controller,
|
|
readOnly: isReadOnly || !isEditMode,
|
|
keyboardType: isNumber ? TextInputType.phone : TextInputType.text,
|
|
style: GoogleFonts.poppins(fontSize: 14),
|
|
decoration: InputDecoration(
|
|
filled: true,
|
|
fillColor: Colors.white,
|
|
contentPadding:
|
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
|
border:
|
|
OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPasswordField(String label, TextEditingController controller) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 15),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(label,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey[600])),
|
|
const SizedBox(height: 5),
|
|
TextField(
|
|
controller: controller,
|
|
obscureText: _obscurePassword,
|
|
readOnly: !isEditMode,
|
|
maxLength: 6,
|
|
style: GoogleFonts.poppins(fontSize: 14),
|
|
decoration: InputDecoration(
|
|
counterText: "",
|
|
filled: true,
|
|
fillColor: Colors.white,
|
|
suffixIcon: IconButton(
|
|
icon: Icon(
|
|
_obscurePassword ? Icons.visibility_off : Icons.visibility),
|
|
onPressed: () =>
|
|
setState(() => _obscurePassword = !_obscurePassword),
|
|
),
|
|
border:
|
|
OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildActionButton(
|
|
{required String label,
|
|
required Color color,
|
|
required VoidCallback onTap}) {
|
|
return OutlinedButton(
|
|
onPressed: onTap,
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: color,
|
|
side: BorderSide(color: color, width: 1.5),
|
|
padding: const EdgeInsets.symmetric(vertical: 15),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
backgroundColor: Colors.white,
|
|
),
|
|
child: Text(label,
|
|
style:
|
|
GoogleFonts.poppins(fontWeight: FontWeight.bold, fontSize: 14)),
|
|
);
|
|
}
|
|
}
|