465 lines
18 KiB
Dart
465 lines
18 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/router/app_route.dart';
|
|
import 'package:niogu_app/core/widgets/custom_empty_screen.dart';
|
|
import 'package:niogu_app/core/widgets/custom_snackbar.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_shimmer.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 CustomerScreen extends ConsumerStatefulWidget {
|
|
const CustomerScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<CustomerScreen> createState() => _CustomerScreenState();
|
|
}
|
|
|
|
class _CustomerScreenState extends ConsumerState<CustomerScreen> {
|
|
final FocusNode _searchFocusNode = FocusNode();
|
|
|
|
Color _searchIconColor = Colors.grey;
|
|
|
|
Timer? _debounce;
|
|
|
|
final TextEditingController _searchController = TextEditingController();
|
|
|
|
@override
|
|
void initState() {
|
|
// TODO: implement initState
|
|
super.initState();
|
|
|
|
_searchFocusNode.addListener(() {
|
|
setState(() {
|
|
_searchIconColor = _searchFocusNode.hasFocus
|
|
? Colors.black
|
|
: Colors.grey;
|
|
});
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_searchController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _onSearchChanged(String value) {
|
|
if (_debounce?.isActive ?? false) _debounce?.cancel();
|
|
_debounce = Timer(const Duration(milliseconds: 800), () {
|
|
ref.read(customerSearchProvider.notifier).state = value;
|
|
});
|
|
}
|
|
|
|
Map<String, List<DisplayCustomers>> _groupedCustomers(
|
|
List<DisplayCustomers> customers,
|
|
) {
|
|
customers.sort((a, b) => a.name.compareTo(b.name));
|
|
|
|
final Map<String, List<DisplayCustomers>> groupedCustomers = {};
|
|
|
|
for (final customer in customers) {
|
|
if (customer.name.isEmpty) continue;
|
|
|
|
String firstLetter = customer.name[0].toUpperCase();
|
|
|
|
if (!RegExp(r'[A-Z]').hasMatch(firstLetter)) {
|
|
firstLetter = '#';
|
|
}
|
|
|
|
if (!groupedCustomers.containsKey(firstLetter)) {
|
|
groupedCustomers[firstLetter] = [];
|
|
}
|
|
groupedCustomers[firstLetter]!.add(customer);
|
|
}
|
|
|
|
return groupedCustomers;
|
|
}
|
|
|
|
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),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final bool isTablet = 100.w >= 600;
|
|
|
|
const greyColor = Color(0xFFF5F5F5);
|
|
|
|
final filteredCustomerState = ref.watch(filteredCustomerProvider);
|
|
|
|
final customerEmptyState = ref.watch(customerEmptyProvider);
|
|
|
|
return SafeArea(
|
|
top: false,
|
|
bottom: true,
|
|
right: false,
|
|
left: false,
|
|
child: Scaffold(
|
|
backgroundColor: const Color(0xFFF9FAFB),
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.white,
|
|
surfaceTintColor: Colors.white,
|
|
elevation: 0,
|
|
toolbarHeight: isTablet ? 7.5.h : kToolbarHeight,
|
|
centerTitle: true,
|
|
leading: 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,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
leadingWidth: 18.w,
|
|
title: Text(
|
|
"Pelanggan",
|
|
style: TextStyle(
|
|
color: Colors.black87,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: AppFontSize.medium.sp,
|
|
),
|
|
),
|
|
actions: [
|
|
IconButton(
|
|
onPressed: () => _showAddOptions(context),
|
|
icon: Icon(
|
|
Icons.add_circle,
|
|
color: AppColor.primaryColor,
|
|
size: 8.w,
|
|
),
|
|
),
|
|
SizedBox(width: 3.w),
|
|
],
|
|
),
|
|
|
|
body: Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 5.w),
|
|
child: Column(
|
|
children: [
|
|
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: _onSearchChanged,
|
|
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(height: 3.h),
|
|
|
|
Expanded(
|
|
child: filteredCustomerState.when(
|
|
data: (customers) {
|
|
final groupedData = _groupedCustomers(customers);
|
|
switch (customerEmptyState) {
|
|
case CustomerEmpty.loading:
|
|
return SizedBox();
|
|
case CustomerEmpty.empty_database:
|
|
return CustomEmptyScreen(
|
|
title: "Tidak Ada Pelanggan",
|
|
body: "Kamu belum memiliki pelanggan",
|
|
);
|
|
case CustomerEmpty.empty_search_result:
|
|
return CustomEmptyScreen(
|
|
body: "Pelanggan Tidak Ditemukan",
|
|
);
|
|
case CustomerEmpty.has_data:
|
|
return ListView.builder(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
padding: EdgeInsets.only(bottom: 5.h),
|
|
itemCount: groupedData.keys.length,
|
|
itemBuilder: (context, index) {
|
|
String letter = groupedData.keys.elementAt(
|
|
index,
|
|
);
|
|
List<DisplayCustomers> customers =
|
|
groupedData[letter]!;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
width: double.infinity,
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 5.w,
|
|
vertical: 1.h,
|
|
),
|
|
color: Colors.grey[200],
|
|
child: Text(
|
|
letter,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.grey[700],
|
|
fontSize: isTablet
|
|
? AppFontSize.medium.sp
|
|
: AppFontSize.small.sp,
|
|
),
|
|
),
|
|
),
|
|
|
|
...customers.map((customer) {
|
|
return Container(
|
|
padding: isTablet
|
|
? EdgeInsets.symmetric(
|
|
vertical: 1.5.h,
|
|
)
|
|
: EdgeInsets.zero,
|
|
color: Colors.white,
|
|
child: Column(
|
|
children: [
|
|
ListTile(
|
|
onTap: () {
|
|
context.pushNamed(
|
|
AppRoute.editCustomerScreen,
|
|
pathParameters: {
|
|
"id": customer.id,
|
|
},
|
|
);
|
|
},
|
|
contentPadding:
|
|
EdgeInsets.symmetric(
|
|
horizontal: 5.w,
|
|
vertical: 0.5.h,
|
|
),
|
|
title: Text(
|
|
customer.name,
|
|
style: TextStyle(
|
|
fontSize: isTablet
|
|
? AppFontSize.medium.sp
|
|
: AppFontSize.small.sp,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
trailing: Icon(
|
|
Icons.chevron_right_rounded,
|
|
size: 5.w,
|
|
color: Colors.grey[400],
|
|
),
|
|
),
|
|
Divider(
|
|
height: 1,
|
|
thickness: 1,
|
|
indent: 5.w,
|
|
color: Colors.grey[100],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
},
|
|
error: (error, stackTrace) {
|
|
return CustomErrorScreen(
|
|
message: "Ups, terjadi kesalahan",
|
|
onRefresh: () {},
|
|
);
|
|
},
|
|
loading: () => const CustomerShimmer(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|