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/constants/app_color.dart';
|
|
import 'package:niogu_app/core/constants/app_font_size.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/supplier/domain/entities/supplier.dart';
|
|
import 'package:niogu_app/features/supplier/presentation/providers/supplier_provider.dart';
|
|
import 'package:niogu_app/features/supplier/presentation/widgets/supplier_shimmer.dart';
|
|
import 'package:sizer/sizer.dart';
|
|
|
|
class SupplierScreen extends ConsumerStatefulWidget {
|
|
const SupplierScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<SupplierScreen> createState() => _SupplierScreenState();
|
|
}
|
|
|
|
class _SupplierScreenState extends ConsumerState<SupplierScreen> {
|
|
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(supplierSearchProvider.notifier).state = value;
|
|
});
|
|
}
|
|
|
|
Map<String, List<DisplaySuppliers>> _groupedSuppliers(
|
|
List<DisplaySuppliers> suppliers,
|
|
) {
|
|
suppliers.sort((a, b) => a.name.compareTo(b.name));
|
|
|
|
final Map<String, List<DisplaySuppliers>> groupedSuppliers = {};
|
|
|
|
for (final supplier in suppliers) {
|
|
if (supplier.name.isEmpty) continue;
|
|
|
|
String firstLetter = supplier.name[0].toUpperCase();
|
|
|
|
if (!RegExp(r'[A-Z]').hasMatch(firstLetter)) {
|
|
firstLetter = '#';
|
|
}
|
|
|
|
if (!groupedSuppliers.containsKey(firstLetter)) {
|
|
groupedSuppliers[firstLetter] = [];
|
|
}
|
|
|
|
groupedSuppliers[firstLetter]!.add(supplier);
|
|
}
|
|
|
|
return groupedSuppliers;
|
|
}
|
|
|
|
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.addSupplierScreen,
|
|
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 Pemasok",
|
|
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 pemasok 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.addSupplierScreen);
|
|
},
|
|
),
|
|
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 filteredSupplierState = ref.watch(filteredSupplierProvider);
|
|
|
|
final supplierEmptyState = ref.watch(supplierEmptyProvider);
|
|
|
|
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(
|
|
"Pemasok",
|
|
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 pemasok",
|
|
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: filteredSupplierState.when(
|
|
data: (suppliers) {
|
|
final groupedData = _groupedSuppliers(suppliers);
|
|
switch (supplierEmptyState) {
|
|
case SupplierEmpty.loading:
|
|
return SizedBox();
|
|
case SupplierEmpty.empty_database:
|
|
return CustomEmptyScreen(
|
|
title: "Tidak Ada Pemasok",
|
|
body: "Kamu belum memiliki pemasok",
|
|
);
|
|
case SupplierEmpty.empty_search_result:
|
|
return CustomEmptyScreen(
|
|
body: "Pemasok Tidak Ditemukan",
|
|
);
|
|
case SupplierEmpty.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<DisplaySuppliers> suppliers =
|
|
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,
|
|
),
|
|
),
|
|
),
|
|
|
|
...suppliers.map((supplier) {
|
|
return Container(
|
|
padding: isTablet
|
|
? EdgeInsets.symmetric(
|
|
vertical: 1.5.h,
|
|
)
|
|
: EdgeInsets.zero,
|
|
color: Colors.white,
|
|
child: Column(
|
|
children: [
|
|
ListTile(
|
|
onTap: () {
|
|
context.pushNamed(
|
|
AppRoute.editSupplierScreen,
|
|
pathParameters: {
|
|
"id": supplier.id,
|
|
},
|
|
);
|
|
},
|
|
contentPadding:
|
|
EdgeInsets.symmetric(
|
|
horizontal: 5.w,
|
|
vertical: 0.5.h,
|
|
),
|
|
title: Text(
|
|
supplier.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 SupplierShimmer(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|