1066 lines
37 KiB
Dart
1066 lines
37 KiB
Dart
import 'dart:io';
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:image_cropper/image_cropper.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:Intaleq/constant/api_key.dart';
|
|
import 'package:Intaleq/constant/box_name.dart';
|
|
import 'package:Intaleq/constant/colors.dart';
|
|
import 'package:Intaleq/controller/profile/profile_controller.dart';
|
|
import 'package:Intaleq/main.dart';
|
|
import 'package:Intaleq/views/widgets/mycircular.dart';
|
|
|
|
import '../../../constant/style.dart';
|
|
import '../../../controller/functions/log_out.dart';
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Main Page
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
class PassengerProfilePage extends StatefulWidget {
|
|
const PassengerProfilePage({super.key});
|
|
|
|
@override
|
|
State<PassengerProfilePage> createState() => _PassengerProfilePageState();
|
|
}
|
|
|
|
class _PassengerProfilePageState extends State<PassengerProfilePage> {
|
|
late final LogOutController logOutController;
|
|
File? _pickedImage;
|
|
bool _isUploadingImage = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
logOutController = Get.put(LogOutController());
|
|
Get.put(ProfileController());
|
|
}
|
|
|
|
// ─── Image Handling ─────────────────────────────────────────────────────────
|
|
|
|
Future<void> _pickAndUploadImage() async {
|
|
// 1. Pick source
|
|
final ImageSource? source = await _showImageSourceSheet();
|
|
if (source == null) return;
|
|
|
|
// 2. Pick image
|
|
final XFile? picked =
|
|
await ImagePicker().pickImage(source: source, imageQuality: 85);
|
|
if (picked == null) return;
|
|
|
|
// 3. Crop
|
|
final CroppedFile? cropped = await ImageCropper().cropImage(
|
|
sourcePath: picked.path,
|
|
aspectRatio: const CropAspectRatio(ratioX: 1, ratioY: 1),
|
|
uiSettings: [
|
|
AndroidUiSettings(
|
|
toolbarTitle: 'Crop Photo'.tr,
|
|
toolbarColor: AppColor.primaryColor,
|
|
toolbarWidgetColor: Colors.white,
|
|
initAspectRatio: CropAspectRatioPreset.square,
|
|
lockAspectRatio: true,
|
|
hideBottomControls: false,
|
|
),
|
|
IOSUiSettings(
|
|
title: 'Crop Photo'.tr,
|
|
aspectRatioLockEnabled: true,
|
|
resetAspectRatioEnabled: false,
|
|
),
|
|
],
|
|
);
|
|
if (cropped == null) return;
|
|
|
|
// 4. Upload
|
|
setState(() {
|
|
_pickedImage = File(cropped.path);
|
|
_isUploadingImage = true;
|
|
});
|
|
|
|
try {
|
|
final passengerId = box.read(BoxName.passengerID).toString();
|
|
final uri = Uri.parse('${AK.serverPHP}/upload_passenger_image.php');
|
|
final request = http.MultipartRequest('POST', uri)
|
|
..fields['passenger_id'] = passengerId
|
|
..files.add(await http.MultipartFile.fromPath('image', cropped.path));
|
|
|
|
final response = await request.send();
|
|
if (response.statusCode == 200) {
|
|
Get.snackbar(
|
|
'Success'.tr,
|
|
'Profile photo updated'.tr,
|
|
backgroundColor: Colors.green.withOpacity(0.9),
|
|
colorText: Colors.white,
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
margin: const EdgeInsets.all(16),
|
|
borderRadius: 12,
|
|
);
|
|
} else {
|
|
_showUploadError();
|
|
}
|
|
} catch (_) {
|
|
_showUploadError();
|
|
} finally {
|
|
setState(() => _isUploadingImage = false);
|
|
}
|
|
}
|
|
|
|
void _showUploadError() {
|
|
Get.snackbar(
|
|
'Error'.tr,
|
|
'Failed to upload photo'.tr,
|
|
backgroundColor: Colors.red.withOpacity(0.9),
|
|
colorText: Colors.white,
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
margin: const EdgeInsets.all(16),
|
|
borderRadius: 12,
|
|
);
|
|
}
|
|
|
|
Future<ImageSource?> _showImageSourceSheet() async {
|
|
return await showModalBottomSheet<ImageSource>(
|
|
context: context,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
),
|
|
builder: (_) => SafeArea(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
margin: const EdgeInsets.only(top: 10, bottom: 6),
|
|
width: 40,
|
|
height: 4,
|
|
decoration: BoxDecoration(
|
|
color: AppColor.grayColor.withOpacity(0.3),
|
|
borderRadius: BorderRadius.circular(2)),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text('Change Photo'.tr,
|
|
style:
|
|
const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 12),
|
|
ListTile(
|
|
leading: Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.primaryColor.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: const Icon(Icons.camera_alt_outlined,
|
|
color: AppColor.primaryColor),
|
|
),
|
|
title: Text('Take a Photo'.tr,
|
|
style: const TextStyle(fontWeight: FontWeight.w500)),
|
|
onTap: () => Navigator.pop(context, ImageSource.camera),
|
|
),
|
|
ListTile(
|
|
leading: Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: const Icon(Icons.photo_library_outlined,
|
|
color: Colors.blue),
|
|
),
|
|
title: Text('Choose from Gallery'.tr,
|
|
style: const TextStyle(fontWeight: FontWeight.w500)),
|
|
onTap: () => Navigator.pop(context, ImageSource.gallery),
|
|
),
|
|
const SizedBox(height: 12),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ─── Build ──────────────────────────────────────────────────────────────────
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: AppColor.secondaryColor.withOpacity(0.96),
|
|
appBar: AppBar(
|
|
title: Text('My Profile'.tr,
|
|
style: AppStyle.headTitle2.copyWith(fontSize: 20)),
|
|
backgroundColor: AppColor.secondaryColor,
|
|
elevation: 0,
|
|
centerTitle: true,
|
|
iconTheme: IconThemeData(color: AppColor.writeColor),
|
|
),
|
|
body: GetBuilder<ProfileController>(
|
|
builder: (controller) {
|
|
if (controller.isloading) {
|
|
return const MyCircularProgressIndicator();
|
|
}
|
|
return ListView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
|
physics: const BouncingScrollPhysics(),
|
|
children: [
|
|
_buildProfileHeader(controller),
|
|
const SizedBox(height: 32),
|
|
_buildSectionCard(
|
|
title: 'Personal Information'.tr,
|
|
sectionIcon: Icons.person_outline_rounded,
|
|
children: [
|
|
_buildTile(
|
|
icon: Icons.badge_outlined,
|
|
color: Colors.blueAccent,
|
|
title: 'Name'.tr,
|
|
subtitle:
|
|
'${controller.prfoileData['first_name'] ?? ''} ${controller.prfoileData['last_name'] ?? ''}'
|
|
.trim(),
|
|
onTap: () => _showNameSheet(controller),
|
|
),
|
|
_divider(),
|
|
_buildTile(
|
|
icon: Icons.wc_outlined,
|
|
color: Colors.pinkAccent,
|
|
title: 'Gender'.tr,
|
|
subtitle: controller.prfoileData['gender']?.toString() ??
|
|
'Not set'.tr,
|
|
onTap: () => _showGenderSheet(controller),
|
|
),
|
|
_divider(),
|
|
_buildTile(
|
|
icon: Icons.school_outlined,
|
|
color: Colors.orangeAccent,
|
|
title: 'Education'.tr,
|
|
subtitle: controller.prfoileData['education']?.toString() ??
|
|
'Not set'.tr,
|
|
onTap: () => _showEducationSheet(controller),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
_buildSectionCard(
|
|
title: 'Work & Contact'.tr,
|
|
sectionIcon: Icons.work_outline_rounded,
|
|
children: [
|
|
_buildTile(
|
|
icon: Icons.work_outline,
|
|
color: Colors.green,
|
|
title: 'Employment Type'.tr,
|
|
subtitle:
|
|
controller.prfoileData['employmentType']?.toString() ??
|
|
'Not set'.tr,
|
|
onTap: () => _showFieldSheet(
|
|
controller: controller,
|
|
fieldKey: 'employmentType',
|
|
title: 'Employment Type'.tr,
|
|
icon: Icons.work_outline,
|
|
iconColor: Colors.green,
|
|
keyboardType: TextInputType.text,
|
|
),
|
|
),
|
|
_divider(),
|
|
_buildTile(
|
|
icon: Icons.favorite_border,
|
|
color: Colors.purpleAccent,
|
|
title: 'Marital Status'.tr,
|
|
subtitle:
|
|
controller.prfoileData['maritalStatus']?.toString() ??
|
|
'Not set'.tr,
|
|
onTap: () => _showFieldSheet(
|
|
controller: controller,
|
|
fieldKey: 'maritalStatus',
|
|
title: 'Marital Status'.tr,
|
|
icon: Icons.favorite_border,
|
|
iconColor: Colors.purpleAccent,
|
|
keyboardType: TextInputType.text,
|
|
),
|
|
),
|
|
_divider(),
|
|
_buildTile(
|
|
icon: Icons.sos_outlined,
|
|
color: Colors.redAccent,
|
|
title: 'SOS Phone'.tr,
|
|
subtitle: controller.prfoileData['sosPhone']?.toString() ??
|
|
'Not set'.tr,
|
|
onTap: () => _showFieldSheet(
|
|
controller: controller,
|
|
fieldKey: 'sosPhone',
|
|
title: 'SOS Phone'.tr,
|
|
icon: Icons.sos_outlined,
|
|
iconColor: Colors.redAccent,
|
|
keyboardType: TextInputType.phone,
|
|
onSaved: () {
|
|
box.write(BoxName.sosPhonePassenger,
|
|
controller.prfoileData['sosPhone']);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
_buildSectionCard(
|
|
title: 'Account'.tr,
|
|
sectionIcon: Icons.manage_accounts_outlined,
|
|
children: [
|
|
_buildTile(
|
|
icon: Icons.logout_rounded,
|
|
color: Colors.blueGrey,
|
|
title: 'Sign Out'.tr,
|
|
subtitle: '',
|
|
onTap: () => logOutController.logOutPassenger(),
|
|
trailing: const SizedBox.shrink(),
|
|
),
|
|
_divider(),
|
|
_buildTile(
|
|
icon: Icons.delete_forever_outlined,
|
|
color: Colors.redAccent,
|
|
title: 'Delete My Account'.tr,
|
|
subtitle: '',
|
|
onTap: () => _showDeleteSheet(),
|
|
trailing: const SizedBox.shrink(),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 32),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
// ─── Profile Header ─────────────────────────────────────────────────────────
|
|
|
|
Widget _buildProfileHeader(ProfileController controller) {
|
|
final firstName = controller.prfoileData['first_name']?.toString() ?? '';
|
|
final lastName = controller.prfoileData['last_name']?.toString() ?? '';
|
|
final fullName = '$firstName $lastName'.trim();
|
|
final passengerId = box.read(BoxName.passengerID)?.toString() ?? '';
|
|
|
|
String email = box.read(BoxName.email) ?? '';
|
|
if (email.contains('intaleqapp.com')) email = '';
|
|
|
|
final initials = fullName.isNotEmpty
|
|
? fullName
|
|
.split(' ')
|
|
.where((e) => e.isNotEmpty)
|
|
.map((e) => e[0].toUpperCase())
|
|
.take(2)
|
|
.join()
|
|
: '?';
|
|
|
|
final avatarUrl =
|
|
'${AK.serverPHP}/portrate_passenger_image/$passengerId.jpg';
|
|
|
|
return Center(
|
|
child: Column(
|
|
children: [
|
|
// ── Avatar with camera button ──────────────────────────────
|
|
Stack(
|
|
alignment: Alignment.bottomRight,
|
|
children: [
|
|
// Outer glow ring
|
|
Container(
|
|
padding: const EdgeInsets.all(3),
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
AppColor.primaryColor.withOpacity(0.6),
|
|
AppColor.primaryColor.withOpacity(0.1),
|
|
],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
),
|
|
child: CircleAvatar(
|
|
radius: 52,
|
|
backgroundColor: AppColor.secondaryColor,
|
|
child: ClipOval(
|
|
child: _isUploadingImage
|
|
? const SizedBox(
|
|
width: 104,
|
|
height: 104,
|
|
child: Center(
|
|
child: CircularProgressIndicator(
|
|
color: AppColor.primaryColor,
|
|
strokeWidth: 2,
|
|
),
|
|
),
|
|
)
|
|
: _pickedImage != null
|
|
? Image.file(
|
|
_pickedImage!,
|
|
width: 104,
|
|
height: 104,
|
|
fit: BoxFit.cover,
|
|
)
|
|
: CachedNetworkImage(
|
|
imageUrl: avatarUrl,
|
|
width: 104,
|
|
height: 104,
|
|
fit: BoxFit.cover,
|
|
placeholder: (_, __) => Container(
|
|
width: 104,
|
|
height: 104,
|
|
color:
|
|
AppColor.primaryColor.withOpacity(0.08),
|
|
child: Center(
|
|
child: Text(
|
|
initials,
|
|
style: const TextStyle(
|
|
fontSize: 34,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColor.primaryColor,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
errorWidget: (_, __, ___) => Container(
|
|
width: 104,
|
|
height: 104,
|
|
color:
|
|
AppColor.primaryColor.withOpacity(0.08),
|
|
child: Center(
|
|
child: Text(
|
|
initials,
|
|
style: const TextStyle(
|
|
fontSize: 34,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColor.primaryColor,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Camera button
|
|
GestureDetector(
|
|
onTap: _isUploadingImage ? null : _pickAndUploadImage,
|
|
child: Container(
|
|
width: 36,
|
|
height: 36,
|
|
margin: const EdgeInsets.only(right: 2, bottom: 2),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.primaryColor,
|
|
shape: BoxShape.circle,
|
|
border:
|
|
Border.all(color: AppColor.secondaryColor, width: 2.5),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: AppColor.primaryColor.withOpacity(0.4),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 3),
|
|
),
|
|
],
|
|
),
|
|
child: const Icon(Icons.camera_alt_rounded,
|
|
color: Colors.white, size: 17),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Name + edit hint
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
fullName.isEmpty ? 'Passenger'.tr : fullName,
|
|
style: TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColor.writeColor,
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
GestureDetector(
|
|
onTap: () => _showNameSheet(Get.find<ProfileController>()),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.primaryColor.withOpacity(0.1),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(Icons.edit_rounded,
|
|
size: 14, color: AppColor.primaryColor),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
if (email.isNotEmpty) ...[
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
email,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: AppColor.grayColor,
|
|
fontWeight: FontWeight.w500),
|
|
),
|
|
],
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
// Passenger badge
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 5),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.primaryColor.withOpacity(0.08),
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(color: AppColor.primaryColor.withOpacity(0.2)),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(Icons.verified_user_rounded,
|
|
size: 13, color: AppColor.primaryColor),
|
|
const SizedBox(width: 5),
|
|
Text(
|
|
'Verified Passenger'.tr,
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: AppColor.primaryColor,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ─── Section Card ────────────────────────────────────────────────────────────
|
|
|
|
Widget _buildSectionCard({
|
|
required String title,
|
|
required IconData sectionIcon,
|
|
required List<Widget> children,
|
|
}) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 8, bottom: 12),
|
|
child: Row(
|
|
children: [
|
|
Icon(sectionIcon, size: 15, color: AppColor.grayColor),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
title.toUpperCase(),
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColor.grayColor,
|
|
letterSpacing: 1.2,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: AppColor.secondaryColor,
|
|
borderRadius: BorderRadius.circular(18),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Get.isDarkMode
|
|
? Colors.black.withOpacity(0.2)
|
|
: Colors.black.withOpacity(0.04),
|
|
blurRadius: 12,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(children: children),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildTile({
|
|
required IconData icon,
|
|
required Color color,
|
|
required String title,
|
|
required String subtitle,
|
|
required VoidCallback onTap,
|
|
Widget? trailing,
|
|
}) {
|
|
return InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(18),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 42,
|
|
height: 42,
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(icon, color: color, size: 21),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(title,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 16,
|
|
color: AppColor.writeColor)),
|
|
if (subtitle.isNotEmpty) ...[
|
|
const SizedBox(height: 4),
|
|
Text(subtitle,
|
|
style:
|
|
TextStyle(color: AppColor.grayColor, fontSize: 13)),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
trailing ??
|
|
Icon(Icons.arrow_forward_ios_rounded,
|
|
size: 14, color: AppColor.grayColor.withOpacity(0.5)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _divider() => Divider(
|
|
height: 1,
|
|
thickness: 1,
|
|
indent: 72,
|
|
endIndent: 16,
|
|
color: AppColor.grayColor.withOpacity(0.1));
|
|
|
|
// ─── Bottom Sheets ───────────────────────────────────────────────────────────
|
|
|
|
/// Name: first + last together
|
|
void _showNameSheet(ProfileController controller) {
|
|
final firstCtrl = TextEditingController(
|
|
text: controller.prfoileData['first_name']?.toString() ?? '');
|
|
final lastCtrl = TextEditingController(
|
|
text: controller.prfoileData['last_name']?.toString() ?? '');
|
|
|
|
_openSheet(
|
|
title: 'Update Name'.tr,
|
|
headerIcon: Icons.badge_outlined,
|
|
headerColor: Colors.blueAccent,
|
|
child: StatefulBuilder(
|
|
builder: (ctx, setStateInner) => Column(
|
|
children: [
|
|
_styledField(
|
|
controller: firstCtrl,
|
|
label: 'First Name'.tr,
|
|
hint: 'Enter your first name'.tr,
|
|
icon: Icons.person_outline,
|
|
type: TextInputType.name,
|
|
),
|
|
const SizedBox(height: 14),
|
|
_styledField(
|
|
controller: lastCtrl,
|
|
label: 'Last Name'.tr,
|
|
hint: 'Enter your last name'.tr,
|
|
icon: Icons.person_outline,
|
|
type: TextInputType.name,
|
|
),
|
|
const SizedBox(height: 24),
|
|
_sheetSaveButton(
|
|
label: 'Save Name'.tr,
|
|
onPressed: () async {
|
|
Get.back();
|
|
// Update both columns sequentially
|
|
await controller.updateColumn({
|
|
'id': box.read(BoxName.passengerID).toString(),
|
|
'first_name': firstCtrl.text.trim(),
|
|
'last_name': lastCtrl.text.trim(),
|
|
});
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Single text field update
|
|
void _showFieldSheet({
|
|
required ProfileController controller,
|
|
required String fieldKey,
|
|
required String title,
|
|
required IconData icon,
|
|
required Color iconColor,
|
|
required TextInputType keyboardType,
|
|
VoidCallback? onSaved,
|
|
}) {
|
|
final tempCtrl = TextEditingController(
|
|
text: controller.prfoileData[fieldKey]?.toString() ?? '');
|
|
|
|
_openSheet(
|
|
title: title,
|
|
headerIcon: icon,
|
|
headerColor: iconColor,
|
|
child: Column(
|
|
children: [
|
|
_styledField(
|
|
controller: tempCtrl,
|
|
label: title,
|
|
hint: '',
|
|
icon: icon,
|
|
type: keyboardType,
|
|
),
|
|
const SizedBox(height: 24),
|
|
_sheetSaveButton(
|
|
onPressed: () async {
|
|
Get.back();
|
|
await controller.updateColumn({
|
|
'id': box.read(BoxName.passengerID).toString(),
|
|
fieldKey: tempCtrl.text.trim(),
|
|
});
|
|
onSaved?.call();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Gender picker sheet
|
|
void _showGenderSheet(ProfileController controller) {
|
|
final options = ['Male'.tr, 'Female'.tr, 'Other'.tr];
|
|
_openSheet(
|
|
title: 'Select Gender'.tr,
|
|
headerIcon: Icons.wc_outlined,
|
|
headerColor: Colors.pinkAccent,
|
|
child: Column(
|
|
children: options
|
|
.map((g) => _choiceTile(
|
|
label: g,
|
|
isSelected: controller.prfoileData['gender']?.toString() == g,
|
|
onTap: () async {
|
|
Get.back();
|
|
await controller.updateColumn({
|
|
'id': box.read(BoxName.passengerID).toString(),
|
|
'gender': g,
|
|
});
|
|
},
|
|
))
|
|
.toList(),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Education picker sheet
|
|
void _showEducationSheet(ProfileController controller) {
|
|
final options = [
|
|
'High School Diploma'.tr,
|
|
'Associate Degree'.tr,
|
|
"Bachelor's Degree".tr,
|
|
"Master's Degree".tr,
|
|
'Doctoral Degree'.tr,
|
|
];
|
|
_openSheet(
|
|
title: 'Select Education'.tr,
|
|
headerIcon: Icons.school_outlined,
|
|
headerColor: Colors.orangeAccent,
|
|
child: Column(
|
|
children: options
|
|
.map((d) => _choiceTile(
|
|
label: d,
|
|
isSelected:
|
|
controller.prfoileData['education']?.toString() == d,
|
|
onTap: () async {
|
|
Get.back();
|
|
await controller.updateColumn({
|
|
'id': box.read(BoxName.passengerID).toString(),
|
|
'education': d,
|
|
});
|
|
},
|
|
))
|
|
.toList(),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Delete account sheet
|
|
void _showDeleteSheet() {
|
|
_openSheet(
|
|
title: 'Delete Account'.tr,
|
|
headerIcon: Icons.delete_forever_outlined,
|
|
headerColor: Colors.redAccent,
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: Colors.red.withOpacity(0.05),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: Colors.red.withOpacity(0.15)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.warning_amber_rounded,
|
|
color: Colors.redAccent, size: 20),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Text(
|
|
'This action is permanent and cannot be undone.'.tr,
|
|
style: TextStyle(
|
|
color: Colors.red.withOpacity(0.8), fontSize: 13),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
_styledField(
|
|
controller: logOutController.emailTextController,
|
|
label: 'Confirm your Email'.tr,
|
|
hint: 'example@domain.com',
|
|
icon: Icons.email_outlined,
|
|
type: TextInputType.emailAddress,
|
|
),
|
|
const SizedBox(height: 24),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
onPressed: () {
|
|
logOutController.emailTextController.clear();
|
|
Get.back();
|
|
},
|
|
style: OutlinedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12)),
|
|
side: BorderSide(color: Colors.grey[300]!),
|
|
),
|
|
child: Text('Cancel'.tr,
|
|
style: TextStyle(color: AppColor.grayColor)),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: ElevatedButton(
|
|
onPressed: () async {
|
|
await logOutController.deletePassengerAccount();
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.redAccent,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12)),
|
|
),
|
|
child: Text('Delete'.tr,
|
|
style: const TextStyle(fontWeight: FontWeight.bold)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ─── Sheet Primitives ────────────────────────────────────────────────────────
|
|
|
|
void _openSheet({
|
|
required String title,
|
|
required IconData headerIcon,
|
|
required Color headerColor,
|
|
required Widget child,
|
|
}) {
|
|
Get.bottomSheet(
|
|
isScrollControlled: true,
|
|
_SheetWrapper(
|
|
title: title,
|
|
headerIcon: headerIcon,
|
|
headerColor: headerColor,
|
|
child: child,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _styledField({
|
|
required TextEditingController controller,
|
|
required String label,
|
|
required String hint,
|
|
required IconData icon,
|
|
required TextInputType type,
|
|
}) {
|
|
return TextField(
|
|
controller: controller,
|
|
keyboardType: type,
|
|
style: TextStyle(fontSize: 16, color: AppColor.writeColor),
|
|
decoration: InputDecoration(
|
|
labelText: label,
|
|
hintText: hint,
|
|
hintStyle:
|
|
TextStyle(color: AppColor.grayColor.withOpacity(0.5), fontSize: 14),
|
|
prefixIcon: Icon(icon, size: 20, color: AppColor.grayColor),
|
|
filled: true,
|
|
fillColor: AppColor.secondaryColor,
|
|
contentPadding:
|
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 15),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(13),
|
|
borderSide:
|
|
BorderSide(color: AppColor.grayColor.withOpacity(0.15))),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(13),
|
|
borderSide:
|
|
BorderSide(color: AppColor.grayColor.withOpacity(0.15))),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(13),
|
|
borderSide: BorderSide(color: AppColor.primaryColor, width: 1.5)),
|
|
labelStyle: TextStyle(color: AppColor.grayColor, fontSize: 14),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _sheetSaveButton({required VoidCallback onPressed, String? label}) {
|
|
return SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed: onPressed,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColor.primaryColor,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(vertical: 15),
|
|
elevation: 0,
|
|
shape:
|
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(13)),
|
|
),
|
|
child: Text(
|
|
label ?? 'Save Changes'.tr,
|
|
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _choiceTile({
|
|
required String label,
|
|
required bool isSelected,
|
|
required VoidCallback onTap,
|
|
}) {
|
|
return ListTile(
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
onTap: onTap,
|
|
leading: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
width: 22,
|
|
height: 22,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: isSelected ? AppColor.primaryColor : Colors.grey[300]!,
|
|
width: 2,
|
|
),
|
|
color: isSelected
|
|
? AppColor.primaryColor.withOpacity(0.1)
|
|
: Colors.transparent,
|
|
),
|
|
child: isSelected
|
|
? const Icon(Icons.check_rounded,
|
|
size: 14, color: AppColor.primaryColor)
|
|
: null,
|
|
),
|
|
title: Text(label,
|
|
style: TextStyle(
|
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
|
fontSize: 16,
|
|
color: isSelected ? AppColor.primaryColor : AppColor.writeColor)),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Reusable Sheet Wrapper
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
class _SheetWrapper extends StatelessWidget {
|
|
final String title;
|
|
final IconData headerIcon;
|
|
final Color headerColor;
|
|
final Widget child;
|
|
|
|
const _SheetWrapper({
|
|
required this.title,
|
|
required this.headerIcon,
|
|
required this.headerColor,
|
|
required this.child,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: EdgeInsets.fromLTRB(
|
|
24,
|
|
16,
|
|
24,
|
|
MediaQuery.of(context).viewInsets.bottom + 28,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.secondaryColor,
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(26)),
|
|
),
|
|
child: SingleChildScrollView(
|
|
physics: const BouncingScrollPhysics(),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Handle bar
|
|
Center(
|
|
child: Container(
|
|
width: 38,
|
|
height: 4,
|
|
margin: const EdgeInsets.only(bottom: 20),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.grayColor.withOpacity(0.3),
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
),
|
|
// Header row
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 42,
|
|
height: 42,
|
|
decoration: BoxDecoration(
|
|
color: headerColor.withOpacity(0.12),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(headerIcon, color: headerColor, size: 22),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColor.writeColor,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
child,
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|