QueenFruits/Mobile Operasional/lib/features/customer/presentation/screens/edit_customer_screen.dart

954 lines
44 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:geolocator/geolocator.dart';
import 'package:go_router/go_router.dart';
import 'package:niogu_app/core/components/top_back_bar_app.dart';
import 'package:niogu_app/core/constants/app_color.dart';
import 'package:niogu_app/core/constants/app_font_size.dart';
import 'package:niogu_app/core/constants/app_temp.dart';
import 'package:niogu_app/core/router/app_route.dart';
import 'package:niogu_app/core/enums/customer_source.dart';
import 'package:niogu_app/core/utils/log_message.dart';
import 'package:niogu_app/core/widgets/custom_snackbar.dart';
import 'package:niogu_app/core/widgets/custom_text_form_field.dart';
import 'package:niogu_app/features/customer/domain/entities/customer.dart';
import 'package:niogu_app/features/customer/presentation/providers/customer_provider.dart';
import 'package:niogu_app/features/customer/presentation/widgets/customer_activity.dart';
import 'package:niogu_app/features/customer/presentation/widgets/edit_customer_shimmer.dart';
import 'package:sizer/sizer.dart';
class EditCustomerScreen extends ConsumerStatefulWidget {
final String customerId;
const EditCustomerScreen({super.key, required this.customerId});
@override
ConsumerState<EditCustomerScreen> createState() => _EditCustomerScreenState();
}
class _EditCustomerScreenState extends ConsumerState<EditCustomerScreen> {
final GlobalKey<FormState> _formKey = GlobalKey();
final TextEditingController _nameController = TextEditingController();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _phoneController = TextEditingController();
bool _isOnlineCustomer = false;
bool _isLoading = true;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(const Duration(milliseconds: 800), () {
_fetchCustomer();
});
});
}
@override
void dispose() {
// TODO: implement dispose
_nameController.dispose();
_emailController.dispose();
_phoneController.dispose();
super.dispose();
}
Future<void> _fetchCustomer() async {
try {
final customerRepository = ref.read(customerRepositoryProvider);
final customer = await customerRepository.fetchCustomerById(
widget.customerId,
);
if (!mounted) return;
_nameController.text = customer.name;
_emailController.text = customer.email;
_phoneController.text = customer.phoneNumber;
_isOnlineCustomer = customer.customerSource == CustomerSource.online;
setState(() {
_isLoading = false;
});
} catch (e, st) {
if (!mounted) return;
setState(() {
_isLoading = false;
});
LogMessage.log.e(e.toString(), error: e, stackTrace: st);
CustomSnackbar.showError(context, "Ups, terjadi kesalahan");
context.pop();
}
}
String calculateDistance(
double startLat,
double startLng,
double endLat,
double endLng,
) {
double distanceInMeters = Geolocator.distanceBetween(
startLat,
startLng,
endLat,
endLng,
);
if (distanceInMeters >= 1000) {
return "${(distanceInMeters / 1000).toStringAsFixed(2)} km";
} else {
return "${distanceInMeters.toStringAsFixed(0)} m";
}
}
Future<void> _updateCustomer() async {
if (!_formKey.currentState!.validate()) return;
try {
await ref
.read(customerControllerProvider.notifier)
.saveCustomer(
UpsertCustomer(
id: widget.customerId,
name: _nameController.text.trim(),
email: _emailController.text.trim(),
phoneNumber: _phoneController.text.trim(),
),
);
if (!mounted) return;
CustomSnackbar.showSuccess(context, "Pelanggan berhasil diubah");
context.pop();
} catch (e, st) {
LogMessage.log.e(e.toString(), error: e, stackTrace: st);
CustomSnackbar.showError(context, "Ups, terjadi kesalahan");
context.pop();
}
}
Future<void> _deleteCustomer() async {
try {
ref
.read(customerControllerProvider.notifier)
.deleteCustomer(widget.customerId);
if (!mounted) return;
CustomSnackbar.showSuccess(context, "Pelanggan berhasil dihapus");
Navigator.pop(context);
context.pop();
} catch (e, st) {
LogMessage.log.e(e.toString(), error: e, stackTrace: st);
CustomSnackbar.showError(context, "Ups, terjadi kesalahan");
Navigator.pop(context);
context.pop();
}
}
void _showDeleteConfirmation(BuildContext context) {
final bool isTablet = 100.w >= 600;
final customerControllerState = ref.watch(customerControllerProvider);
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4.5.w),
),
elevation: 0,
backgroundColor: Colors.transparent,
child: Container(
padding: EdgeInsets.all(6.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4.5.w),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: Colors.red[50],
shape: BoxShape.circle,
),
child: Icon(
Icons.warning_amber_rounded,
color: Colors.red,
size: 10.w,
),
),
SizedBox(height: 2.h),
Text(
"Hapus Pelanggan?",
style: TextStyle(
fontSize: AppFontSize.medium.sp,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
SizedBox(height: 1.h),
Text(
"Tindakan ini tidak dapat dibatalkan. Semua data profil pelanggan akan dihapus dari sistem.",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: isTablet
? (AppFontSize.medium - 1.25).sp
: (AppFontSize.small - 1.25).sp,
color: Colors.grey[600],
height: 1.5,
),
),
SizedBox(height: 3.h),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey.shade300,
padding: EdgeInsets.symmetric(vertical: 1.5.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(2.5.w),
),
elevation: 0,
),
child: Text(
"Batal",
style: TextStyle(
color: Colors.grey[800],
fontWeight: FontWeight.bold,
fontSize: AppFontSize.medium.sp,
),
),
),
),
SizedBox(width: 3.w),
Expanded(
child: ElevatedButton(
onPressed: customerControllerState.isLoading
? null
: _deleteCustomer,
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 1.5.h),
backgroundColor: Colors.red,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(2.5.w),
),
disabledBackgroundColor: Colors.grey.shade300,
),
child: Text(
"Ya, Hapus",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: AppFontSize.medium.sp,
),
),
),
),
],
),
],
),
),
);
},
);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final bool isTablet = 100.w >= 600;
final customerAddressesState = ref.watch(
customerAddressesProvider(widget.customerId),
);
final customerActivitiesState = ref.watch(
customerActivitiesProvider(widget.customerId),
);
final customerControllerState = ref.watch(customerControllerProvider);
return SafeArea(
top: false,
bottom: true,
right: false,
left: false,
child: Scaffold(
backgroundColor: Colors.white,
appBar: TopBackBarApp(
title: "Detail Pelanggan",
onTap: () => context.pop(),
),
body: _isLoading
? const EditCustomerShimmer()
: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Form(
key: _formKey,
child: Column(
children: [
Container(
width: double.infinity,
color: Colors.white,
padding: EdgeInsets.symmetric(vertical: 3.h),
child: Column(
children: [
Stack(
alignment: Alignment.bottomRight,
children: [
CircleAvatar(
radius: 12.w,
backgroundColor: Colors.grey[200],
child: Icon(
Icons.person,
size: 15.w,
color: Colors.grey[400],
),
),
Container(
padding: EdgeInsets.all(1.5.w),
decoration: BoxDecoration(
color: _isOnlineCustomer
? Colors.blue
: Colors.orange,
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: 2,
),
),
child: Icon(
_isOnlineCustomer
? Icons.language
: Icons.storefront,
size: 5.w,
color: Colors.white,
),
),
],
),
SizedBox(height: 1.5.h),
Container(
padding: EdgeInsets.symmetric(
horizontal: 3.w,
vertical: 0.5.h,
),
decoration: BoxDecoration(
color: _isOnlineCustomer
? Colors.blue[50]
: Colors.orange[50],
borderRadius: BorderRadius.circular(4.5.w),
),
child: Text(
_isOnlineCustomer
? "Pelanggan Toko Online"
: "Pelanggan Offline",
style: TextStyle(
fontSize: isTablet
? (AppFontSize.medium - 1.25).sp
: (AppFontSize.small - 1.25).sp,
fontWeight: FontWeight.bold,
color: _isOnlineCustomer
? Colors.blue[700]
: Colors.orange[700],
),
),
),
],
),
),
Container(
color: Colors.white,
padding: EdgeInsets.fromLTRB(5.w, 0, 5.w, 3.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomTextFormField(
label: "Nama Pelanggan",
controller: _nameController,
hint: "Masukkan nama",
validator: (value) {
if (value == null || value.isEmpty) {
return "Nama pelanggan belum diisi";
}
return null;
},
prefixIcon: Icons.person_outline,
readOnly: _isOnlineCustomer,
),
SizedBox(height: 2.h),
CustomTextFormField(
label: "Email",
controller: _emailController,
hint: "Masukkan email (opsional)",
keyboardType: TextInputType.emailAddress,
prefixIcon: Icons.email_outlined,
readOnly: _isOnlineCustomer,
),
SizedBox(height: 2.h),
CustomTextFormField(
label: "No. Handphone / WA",
controller: _phoneController,
hint: "Masukkan no handphone / wa (opsional)",
keyboardType: TextInputType.phone,
prefixIcon: Icons.phone_android_outlined,
readOnly: _isOnlineCustomer,
),
],
),
),
_buildSectionHeader("Alamat Pelanggan"),
customerAddressesState.when(
data: (addresses) {
final int displayAddressCount =
addresses.length > 3 ? 3 : addresses.length;
final bool hasMore = addresses.length > 3;
return addresses.isEmpty
? _buildEmptyOrErrorAddress(
Icons.location_off_outlined,
"Tidak ada alamat yang tersedia",
)
: SizedBox(
height: 22.5.h,
child: ListView.builder(
padding: EdgeInsets.only(
left: 5.w,
right: 2.w,
),
scrollDirection: Axis.horizontal,
itemCount:
displayAddressCount +
(hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index < displayAddressCount) {
final address = addresses[index];
final String distance =
calculateDistance(
AppTemp.OUTLET_LATITUDE,
AppTemp.OUTLET_LONGITUDE,
address.coordinate.latitude,
address.coordinate.longitude,
);
return Container(
width: 75.w,
margin: EdgeInsets.only(
right: 3.w,
),
padding: EdgeInsets.all(3.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
BorderRadius.circular(
2.5.w,
),
border: Border.all(
color: Colors.grey[200]!,
),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: EdgeInsets.all(
2.w,
),
decoration:
BoxDecoration(
color: AppColor
.primaryColor
.withOpacity(
0.1,
),
shape: BoxShape
.circle,
),
child: Icon(
Icons.location_on,
color: AppColor
.primaryColor,
size: 5.w,
),
),
SizedBox(width: 3.w),
Text(
address.label,
style: TextStyle(
fontSize: isTablet
? (AppFontSize.medium -
1.25)
.sp
: (AppFontSize.small -
1.25)
.sp,
color:
Colors.grey[700],
),
),
],
),
SizedBox(height: 1.5.h),
Text(
address.fullAddress,
overflow:
TextOverflow.ellipsis,
style: TextStyle(
fontSize: isTablet
? (AppFontSize.medium -
1.25)
.sp
: (AppFontSize.small -
1.25)
.sp,
color: Colors.grey[700],
height: 1.4,
),
),
Divider(
color: Colors.grey[100],
),
Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Row(
children: [
Icon(
Icons
.straighten_rounded,
size: 5.w,
color: Colors.grey,
),
SizedBox(width: 3.w),
Text(
"$distance",
style: TextStyle(
fontSize: isTablet
? (AppFontSize.medium -
1.25)
.sp
: (AppFontSize.small -
1.25)
.sp,
color:
Colors.black,
fontWeight:
FontWeight
.bold,
),
),
],
),
TextButton.icon(
onPressed: () {
context.pushNamed(
AppRoute
.mapCustomerAddressScreen,
extra: address,
);
},
icon: Icon(
Icons.map_outlined,
color: AppColor
.primaryColor,
size: 5.w,
),
label: Text(
"Lihat di Peta",
style: TextStyle(
color: AppColor
.primaryColor,
fontSize:
AppFontSize
.small
.sp,
fontWeight:
FontWeight.bold,
),
),
),
],
),
],
),
);
} else {
return GestureDetector(
onTap: () async {
FocusScope.of(
context,
).unfocus();
await Future.delayed(
const Duration(
milliseconds: 400,
),
);
context.pushNamed(
AppRoute
.customerAddressesScreen,
extra: addresses,
);
},
child: Container(
width: 35.w,
margin: EdgeInsets.only(
right: 5.w,
),
decoration: BoxDecoration(
color: AppColor.primaryColor
.withOpacity(0.05),
borderRadius:
BorderRadius.circular(
2.5.w,
),
border: Border.all(
color: AppColor.primaryColor
.withOpacity(0.2),
),
),
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(
Icons
.arrow_circle_right_outlined,
color:
AppColor.primaryColor,
size: 8.w,
),
SizedBox(height: 0.5.h),
Text(
"${addresses.length - 3} Alamat\nLainnya",
textAlign:
TextAlign.center,
style: TextStyle(
color: AppColor
.primaryColor,
fontWeight:
FontWeight.bold,
fontSize: isTablet
? (AppFontSize.medium -
1.25)
.sp
: (AppFontSize.small -
1.25)
.sp,
),
),
],
),
),
);
}
},
),
);
},
error: (error, stackTrace) =>
_buildEmptyOrErrorAddress(
Icons.error_rounded,
"Ups, terjadi kesalahan",
),
loading: () => const SizedBox(),
),
_buildSectionHeader("Aktivitas Pelanggan"),
customerActivitiesState.when(
data: (activities) {
final int totalActivity = activities.length;
final int displayActivityCount = totalActivity > 3
? 3
: totalActivity;
return activities.isEmpty
? _buildEmptyOrErrorActivities(
Icons.history_rounded,
"Belum ada riwayat aktivitas pembelian",
)
: Column(
children: [
ListView.builder(
shrinkWrap: true,
physics:
const NeverScrollableScrollPhysics(),
padding: EdgeInsets.symmetric(
horizontal: 5.w,
),
itemCount: displayActivityCount,
itemBuilder: (context, index) {
final activity = activities[index];
return CustomerActivity(
isOnlineCustomer:
_isOnlineCustomer,
activity: activity,
);
},
),
if (totalActivity > 3)
Padding(
padding: EdgeInsets.symmetric(
horizontal: 5.w,
),
child: InkWell(
onTap: () {
context.pushNamed(
AppRoute
.customerActivitesScreen,
extra: {
"is_online_customer":
_isOnlineCustomer,
"activities": activities,
},
);
},
borderRadius:
BorderRadius.circular(2.5.w),
child: Container(
padding: EdgeInsets.symmetric(
vertical: 1.5.h,
),
decoration: BoxDecoration(
border: Border.all(
color: Colors.grey[300]!,
),
borderRadius:
BorderRadius.circular(
2.5.w,
),
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
"Lihat ${totalActivity - 3} Aktivitas Lainnya",
style: TextStyle(
color: Colors.grey[600],
fontWeight:
FontWeight.bold,
fontSize: isTablet
? (AppFontSize.medium -
1.25)
.sp
: (AppFontSize.small -
1.25)
.sp,
),
),
SizedBox(width: 2.w),
Icon(
Icons
.keyboard_arrow_down_rounded,
color: Colors.grey[600],
size: 5.w,
),
],
),
),
),
),
],
);
},
error: (error, stackTrace) =>
_buildEmptyOrErrorActivities(
Icons.error_rounded,
"Ups, terjadi kesalahan",
),
loading: () => const SizedBox(),
),
if (!_isOnlineCustomer) ...[
SizedBox(height: 5.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 5.w),
child: TextButton.icon(
onPressed: () =>
_showDeleteConfirmation(context),
style: TextButton.styleFrom(
foregroundColor: Colors.red[700],
padding: EdgeInsets.symmetric(
vertical: 1.5.h,
horizontal: 4.w,
),
),
icon: Icon(
Icons.delete_outline_rounded,
size: 5.w,
),
label: Text(
"Hapus Pelanggan Ini",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: isTablet
? (AppFontSize.medium - 1.25).sp
: (AppFontSize.small - 1.25).sp,
),
),
),
),
],
SizedBox(height: 4.h),
],
),
),
),
bottomNavigationBar: _isLoading
? null
: Container(
padding: EdgeInsets.all(5.w),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, -5),
),
],
),
child: ElevatedButton(
onPressed:
_isOnlineCustomer || customerControllerState.isLoading
? null
: _updateCustomer,
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primaryColor,
minimumSize: Size(double.infinity, 6.5.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(2.5.w),
),
disabledBackgroundColor: Colors.grey.shade300,
),
child: Text(
"Perbarui Informasi Pelanggan",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: AppFontSize.medium.sp,
color: Colors.white,
),
),
),
),
),
);
},
);
}
Widget _buildSectionHeader(String title) {
return Padding(
padding: EdgeInsets.fromLTRB(5.w, 3.h, 5.w, 1.5.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: (AppFontSize.medium - 1.25).sp,
),
),
],
),
);
}
Widget _buildEmptyOrErrorAddress(IconData icon, String body) {
return Container(
height: 15.h,
width: double.infinity,
margin: EdgeInsets.symmetric(horizontal: 5.w),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(2.5.w),
border: Border.all(
color: Colors.grey.shade200,
style: BorderStyle.solid,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: Colors.grey[400], size: 8.w),
SizedBox(height: 1.h),
Text(
body,
style: TextStyle(
fontSize: (AppFontSize.medium - 1.25).sp,
color: Colors.grey[500],
),
),
],
),
);
}
Widget _buildEmptyOrErrorActivities(IconData icon, String body) {
return Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 4.h),
margin: EdgeInsets.symmetric(horizontal: 5.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(2.5.w),
border: Border.all(color: Colors.grey.shade100),
),
child: Column(
children: [
Icon(icon, color: Colors.grey[300], size: 10.w),
SizedBox(height: 1.5.h),
Text(
body,
style: TextStyle(
fontSize: (AppFontSize.medium - 1.25).sp,
color: Colors.grey[500],
),
),
],
),
);
}
}