744 lines
33 KiB
Dart
744 lines
33 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:niogu_app/core/widgets/custom_error_screen.dart';
|
|
import 'package:niogu_app/core/providers/app_provider.dart';
|
|
import 'package:niogu_app/core/router/app_route.dart';
|
|
import 'package:niogu_app/core/enums/user_role.dart';
|
|
import 'package:niogu_app/core/widgets/custom_empty_screen.dart';
|
|
import 'package:niogu_app/core/widgets/custom_snackbar.dart';
|
|
import 'package:niogu_app/core/widgets/pop_up_notification.dart';
|
|
import 'package:niogu_app/features/pos/domain/entities/pos.dart';
|
|
import 'package:niogu_app/features/pos/presentation/providers/pos_provider.dart';
|
|
import 'package:niogu_app/features/pos/presentation/widgets/already_customer_shimmer.dart';
|
|
import 'package:niogu_app/features/pos/presentation/widgets/checkbox_customer.dart';
|
|
import 'package:sizer/sizer.dart';
|
|
import 'package:niogu_app/core/constants/app_color.dart';
|
|
import 'package:niogu_app/core/constants/app_font_size.dart';
|
|
|
|
class AlreadyCustomerScreen extends ConsumerStatefulWidget {
|
|
const AlreadyCustomerScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<AlreadyCustomerScreen> createState() =>
|
|
_AlreadyCustomerScreenState();
|
|
}
|
|
|
|
class _AlreadyCustomerScreenState extends ConsumerState<AlreadyCustomerScreen> {
|
|
final FocusNode _searchFocusNode = FocusNode();
|
|
|
|
Color _searchIconColor = Colors.grey;
|
|
|
|
Timer? _debounce;
|
|
|
|
String? _selectedCustomerId;
|
|
|
|
String? _selectedCustomerAddress;
|
|
|
|
SelectedCustomer? _selectedCustomer;
|
|
|
|
@override
|
|
void initState() {
|
|
// TODO: implement initState
|
|
super.initState();
|
|
|
|
_searchFocusNode.addListener(() {
|
|
setState(() {
|
|
_searchIconColor = _searchFocusNode.hasFocus
|
|
? Colors.black
|
|
: Colors.grey;
|
|
});
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
// TODO: implement dispose
|
|
_searchFocusNode.dispose();
|
|
_debounce?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
void _onSearchChanged(String value) {
|
|
if (_debounce?.isActive ?? false) _debounce?.cancel();
|
|
_debounce = Timer(const Duration(milliseconds: 800), () {
|
|
ref.read(alreadyCustomerSearchProvider.notifier).state = value;
|
|
});
|
|
}
|
|
|
|
Future<void> _handleImportContact() async {
|
|
final permission = await FlutterContacts.requestPermission();
|
|
if (permission) {
|
|
final contact = await FlutterContacts.openExternalPick();
|
|
|
|
if (contact != null) {
|
|
String name = contact.displayName;
|
|
String phone = "";
|
|
|
|
if (contact.phones.isNotEmpty) {
|
|
phone = contact.phones.first.number;
|
|
}
|
|
|
|
if (!mounted) return;
|
|
|
|
context.pushNamed(
|
|
AppRoute.addCustomerScreen,
|
|
extra: {'name': name, 'phone': phone},
|
|
);
|
|
}
|
|
} else {
|
|
CustomSnackbar.showWarning(context, "Akses Ditolak");
|
|
}
|
|
}
|
|
|
|
void _showAddOptions(BuildContext context) {
|
|
final bool isTablet = 100.w >= 600;
|
|
showModalBottomSheet(
|
|
context: context,
|
|
backgroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(2.5.w)),
|
|
),
|
|
constraints: const BoxConstraints(maxWidth: double.infinity),
|
|
builder: (context) {
|
|
return SafeArea(
|
|
child: Container(
|
|
width: 100.w,
|
|
padding: EdgeInsets.symmetric(vertical: 2.h),
|
|
clipBehavior: Clip.hardEdge,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(6.w)),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Padding(
|
|
padding: EdgeInsets.fromLTRB(5.w, 2.h, 5.w, 1.h),
|
|
child: Text(
|
|
"Tambah Pelanggan",
|
|
style: TextStyle(
|
|
fontSize: AppFontSize.medium.sp,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 2.h),
|
|
ListTile(
|
|
leading: Padding(
|
|
padding: EdgeInsets.only(left: 5.w),
|
|
child: Icon(
|
|
Icons.edit_note_rounded,
|
|
color: Colors.blue,
|
|
size: 6.w,
|
|
),
|
|
),
|
|
title: Text(
|
|
"Input Manual",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: isTablet
|
|
? AppFontSize.medium.sp
|
|
: AppFontSize.small.sp,
|
|
),
|
|
),
|
|
subtitle: Text(
|
|
"Isi informasi pelanggan secara manual",
|
|
style: TextStyle(
|
|
fontSize: isTablet
|
|
? (AppFontSize.medium - 1.25).sp
|
|
: (AppFontSize.small - 1.25).sp,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
onTap: () {
|
|
context.pop();
|
|
context.pushNamed(AppRoute.addCustomerScreen);
|
|
},
|
|
),
|
|
SizedBox(height: 2.h),
|
|
ListTile(
|
|
leading: Padding(
|
|
padding: EdgeInsets.only(left: 5.w),
|
|
child: Icon(
|
|
Icons.contacts_rounded,
|
|
color: Colors.green,
|
|
size: 6.w,
|
|
),
|
|
),
|
|
title: Text(
|
|
"Ambil dari Kontak HP",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: isTablet
|
|
? AppFontSize.medium.sp
|
|
: AppFontSize.small.sp,
|
|
),
|
|
),
|
|
subtitle: Text(
|
|
"Pilih langsung dari buku telepon",
|
|
style: TextStyle(
|
|
fontSize: isTablet
|
|
? (AppFontSize.medium - 1.25).sp
|
|
: (AppFontSize.small - 1.25).sp,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
onTap: () async {
|
|
context.pop();
|
|
await _handleImportContact();
|
|
},
|
|
),
|
|
SizedBox(height: 2.h),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
void _selectCustomer(
|
|
SelectedCustomer selectedCustomer,
|
|
String customerId,
|
|
String address,
|
|
) {
|
|
setState(() {
|
|
if (_selectedCustomerId == customerId &&
|
|
_selectedCustomerAddress == address) {
|
|
_selectedCustomerId = null;
|
|
_selectedCustomerAddress = null;
|
|
_selectedCustomer = null;
|
|
} else {
|
|
_selectedCustomerId = customerId;
|
|
_selectedCustomerAddress = address;
|
|
_selectedCustomer = selectedCustomer;
|
|
}
|
|
});
|
|
}
|
|
|
|
void _submitSelection() {
|
|
ref.read(selectedCustomerProvider.notifier).state = _selectedCustomer;
|
|
context.pop();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final bool isTablet = 100.w >= 600;
|
|
|
|
const greyColor = Color(0xFFF5F5F5);
|
|
|
|
final messageActivity = ref.watch(messageActivityProvider);
|
|
|
|
final currentUserRole = ref.watch(currentUserRoleProvider);
|
|
|
|
final filteredCustomerState = ref.watch(
|
|
filteredAlreadyCustomerProvider,
|
|
);
|
|
|
|
final customerEmptyState = ref.watch(alreadyCustomerEmptyProvider);
|
|
|
|
return SafeArea(
|
|
top: false,
|
|
bottom: true,
|
|
right: false,
|
|
left: false,
|
|
child: Stack(
|
|
children: [
|
|
Scaffold(
|
|
backgroundColor: Colors.grey[50],
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.white,
|
|
surfaceTintColor: Colors.white,
|
|
elevation: 0,
|
|
toolbarHeight: 10.h,
|
|
titleSpacing: 0,
|
|
automaticallyImplyLeading: false,
|
|
title: Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
|
child: Row(
|
|
children: [
|
|
Center(
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
type: MaterialType.canvas,
|
|
child: InkWell(
|
|
onTap: () => context.pop(),
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
child: Container(
|
|
width: 10.w,
|
|
height: 10.w,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: Border.all(
|
|
color: Colors.grey.shade200,
|
|
),
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 5,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Icon(
|
|
Icons.arrow_back_ios_new_rounded,
|
|
color: Colors.black87,
|
|
size: 5.w,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
SizedBox(width: 3.w),
|
|
|
|
Expanded(
|
|
child: Container(
|
|
height: 6.h,
|
|
padding: isTablet
|
|
? EdgeInsets.symmetric(
|
|
vertical: 1.h,
|
|
horizontal: 2.w,
|
|
)
|
|
: EdgeInsets.zero,
|
|
decoration: BoxDecoration(
|
|
color: greyColor,
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
),
|
|
child: TextField(
|
|
focusNode: _searchFocusNode,
|
|
onChanged: (value) => _onSearchChanged(value),
|
|
textAlignVertical: TextAlignVertical.center,
|
|
style: TextStyle(
|
|
color: Colors.black87,
|
|
fontSize: isTablet
|
|
? AppFontSize.medium.sp
|
|
: AppFontSize.small.sp,
|
|
),
|
|
decoration: InputDecoration(
|
|
hintText: "Cari nama pelanggan...",
|
|
hintStyle: TextStyle(
|
|
color: _searchIconColor,
|
|
fontSize: isTablet
|
|
? AppFontSize.medium.sp
|
|
: AppFontSize.small.sp,
|
|
),
|
|
prefixIcon: Icon(
|
|
Icons.search,
|
|
color: _searchIconColor,
|
|
size: 5.w,
|
|
),
|
|
border: InputBorder.none,
|
|
contentPadding: EdgeInsets.zero,
|
|
isDense: true,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
SizedBox(width: 2.w),
|
|
|
|
IconButton(
|
|
onPressed: () => _showAddOptions(context),
|
|
icon: Icon(
|
|
Icons.add_circle,
|
|
color: AppColor.primaryColor,
|
|
size: 8.w,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
bottom: PreferredSize(
|
|
preferredSize: Size.fromHeight(2),
|
|
child: Container(color: Colors.grey.shade100, height: 1),
|
|
),
|
|
),
|
|
body: filteredCustomerState.when(
|
|
data: (customers) {
|
|
switch (customerEmptyState) {
|
|
case AlreadyCustomerEmpty.loading:
|
|
return SizedBox();
|
|
case AlreadyCustomerEmpty.empty_database:
|
|
return CustomEmptyScreen(
|
|
title: "Tidak Ada Pelanggan",
|
|
body: "Kamu belum memiliki pelanggan",
|
|
);
|
|
|
|
case AlreadyCustomerEmpty.empty_search_result:
|
|
return CustomEmptyScreen(
|
|
body: "Pelanggan Tidak Ditemukan",
|
|
);
|
|
case AlreadyCustomerEmpty.has_data:
|
|
return ListView.builder(
|
|
padding: EdgeInsets.symmetric(
|
|
vertical: 2.h,
|
|
horizontal: 5.w,
|
|
),
|
|
itemCount: customers.length,
|
|
itemBuilder: (context, index) {
|
|
final customer = customers[index];
|
|
|
|
final bool hasMultiAddress =
|
|
customer.addresses.length > 1;
|
|
|
|
final bool isSingleSelected =
|
|
!hasMultiAddress &&
|
|
_selectedCustomerId == customer.id;
|
|
|
|
final bool isExpanded = ref.watch(
|
|
expandedAlreadyCustomerProvider(customer.id),
|
|
);
|
|
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
margin: EdgeInsets.only(bottom: 2.h),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(4.w),
|
|
border: Border.all(
|
|
color: Colors.grey.shade200,
|
|
width: 1,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.03),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
InkWell(
|
|
onTap: () {
|
|
if (hasMultiAddress) {
|
|
ref
|
|
.read(
|
|
expandedAlreadyCustomerProvider(
|
|
customer.id,
|
|
).notifier,
|
|
)
|
|
.state = !ref.read(
|
|
expandedAlreadyCustomerProvider(
|
|
customer.id,
|
|
),
|
|
);
|
|
} else {
|
|
_selectCustomer(
|
|
SelectedCustomer(
|
|
id: customer.id,
|
|
outletId: ref.read(
|
|
currentOutletIdProvider,
|
|
)!,
|
|
name: customer.name,
|
|
phoneNumber: customer.phoneNumber,
|
|
address: customer.addresses.first,
|
|
),
|
|
customer.id,
|
|
customer.addresses.first,
|
|
);
|
|
}
|
|
},
|
|
borderRadius: BorderRadius.vertical(
|
|
top: Radius.circular(4.w),
|
|
bottom: (hasMultiAddress && isExpanded)
|
|
? Radius.zero
|
|
: Radius.circular(4.w),
|
|
),
|
|
child: Padding(
|
|
padding: EdgeInsets.all(3.w),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 12.w,
|
|
height: 12.w,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[100],
|
|
shape: BoxShape.circle,
|
|
),
|
|
alignment: Alignment.center,
|
|
child: Text(
|
|
customer.name[0].toUpperCase(),
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: isTablet
|
|
? AppFontSize.medium.sp
|
|
: AppFontSize.small.sp,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 4.w),
|
|
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
customer.name,
|
|
style: TextStyle(
|
|
fontSize: isTablet
|
|
? AppFontSize.medium.sp
|
|
: AppFontSize.small.sp,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
if (customer
|
|
.phoneNumber
|
|
.isNotEmpty) ...[
|
|
SizedBox(height: 0.75.h),
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.phone_android,
|
|
size: 5.w,
|
|
color: Colors.grey,
|
|
),
|
|
SizedBox(width: 1.w),
|
|
Text(
|
|
customer.phoneNumber,
|
|
style: TextStyle(
|
|
fontSize: isTablet
|
|
? AppFontSize
|
|
.medium
|
|
.sp
|
|
: AppFontSize
|
|
.small
|
|
.sp,
|
|
color:
|
|
Colors.grey[600],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
|
|
if (customer
|
|
.addresses
|
|
.first
|
|
.isNotEmpty &&
|
|
!hasMultiAddress) ...[
|
|
SizedBox(height: 0.75.h),
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons
|
|
.location_on_outlined,
|
|
color: Colors.grey,
|
|
size: 5.w,
|
|
),
|
|
|
|
SizedBox(width: 1.w),
|
|
|
|
Expanded(
|
|
child: Text(
|
|
customer
|
|
.addresses
|
|
.first,
|
|
overflow: TextOverflow
|
|
.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: isTablet
|
|
? AppFontSize
|
|
.medium
|
|
.sp
|
|
: AppFontSize
|
|
.small
|
|
.sp,
|
|
color: Colors
|
|
.grey[500],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
|
|
if (customer
|
|
.addresses
|
|
.isNotEmpty) ...[
|
|
if (hasMultiAddress)
|
|
Icon(
|
|
isExpanded
|
|
? Icons
|
|
.keyboard_arrow_up_rounded
|
|
: Icons
|
|
.keyboard_arrow_down_rounded,
|
|
color: Colors.grey,
|
|
size: 7.w,
|
|
)
|
|
else
|
|
CheckboxCustomer(
|
|
isSelected: isSingleSelected,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
if (hasMultiAddress && isExpanded) ...[
|
|
Container(
|
|
height: 1,
|
|
color: Colors.grey[100],
|
|
),
|
|
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
vertical: 1.h,
|
|
),
|
|
child: Column(
|
|
children: customer.addresses.map((
|
|
address,
|
|
) {
|
|
final bool isAddressSelected =
|
|
_selectedCustomerId ==
|
|
customer.id &&
|
|
_selectedCustomerAddress ==
|
|
address;
|
|
|
|
return InkWell(
|
|
onTap: () {
|
|
_selectCustomer(
|
|
SelectedCustomer(
|
|
id: customer.id,
|
|
outletId: ref.read(
|
|
currentOutletIdProvider,
|
|
)!,
|
|
name: customer.name,
|
|
phoneNumber:
|
|
customer.phoneNumber,
|
|
address: address,
|
|
),
|
|
customer.id,
|
|
address,
|
|
);
|
|
},
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 4.w,
|
|
vertical: 1.5.h,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
SizedBox(width: 4.w),
|
|
|
|
CheckboxCustomer(
|
|
isSelected:
|
|
isAddressSelected,
|
|
),
|
|
|
|
SizedBox(width: 3.w),
|
|
|
|
Expanded(
|
|
child: Text(
|
|
address,
|
|
overflow:
|
|
TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: isTablet
|
|
? AppFontSize
|
|
.medium
|
|
.sp
|
|
: AppFontSize
|
|
.small
|
|
.sp,
|
|
color: isAddressSelected
|
|
? Colors.black
|
|
: Colors.grey[700],
|
|
fontWeight:
|
|
isAddressSelected
|
|
? FontWeight.bold
|
|
: FontWeight.normal,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
},
|
|
error: (error, stackTrace) {
|
|
return CustomErrorScreen(
|
|
message: "Ups, terjadi kesalahan",
|
|
onRefresh: () {},
|
|
);
|
|
},
|
|
loading: () => const AlreadyCustomerShimmer(),
|
|
),
|
|
bottomNavigationBar: 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: SizedBox(
|
|
height: 6.5.h,
|
|
child: ElevatedButton(
|
|
onPressed: (_selectedCustomerId != null)
|
|
? _submitSelection
|
|
: null,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColor.primaryColor,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(2.5.w),
|
|
),
|
|
elevation: 0,
|
|
disabledBackgroundColor: Colors.grey.shade300,
|
|
),
|
|
child: Text(
|
|
"Pilih Pelanggan",
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: AppFontSize.medium.sp,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
if (messageActivity != null)
|
|
PopupNotification(
|
|
isOwner: currentUserRole == UserRole.owner,
|
|
messages: messageActivity.messages,
|
|
type: messageActivity.type,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|