2025-08-15 16:31:29 +07:00
|
|
|
import 'package:auto_route/auto_route.dart';
|
|
|
|
|
import 'package:flutter/material.dart';
|
2025-08-18 00:30:17 +07:00
|
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
2025-08-15 16:31:29 +07:00
|
|
|
import 'package:line_icons/line_icons.dart';
|
|
|
|
|
|
2025-08-18 00:30:17 +07:00
|
|
|
import '../../../application/customer/customer_loader/customer_loader_bloc.dart';
|
2025-08-15 16:31:29 +07:00
|
|
|
import '../../../common/theme/theme.dart';
|
2025-08-18 00:30:17 +07:00
|
|
|
import '../../../domain/customer/customer.dart';
|
|
|
|
|
import '../../../injection.dart';
|
2025-08-16 00:39:09 +07:00
|
|
|
import '../../components/appbar/appbar.dart';
|
2025-08-15 16:31:29 +07:00
|
|
|
import '../../components/button/button.dart';
|
|
|
|
|
import 'widgets/customer_card.dart';
|
|
|
|
|
import 'widgets/customer_tile.dart';
|
|
|
|
|
|
|
|
|
|
@RoutePage()
|
2025-08-18 00:30:17 +07:00
|
|
|
class CustomerPage extends StatefulWidget implements AutoRouteWrapper {
|
2025-08-15 16:31:29 +07:00
|
|
|
const CustomerPage({super.key});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<CustomerPage> createState() => _CustomerPageState();
|
2025-08-18 00:30:17 +07:00
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget wrappedRoute(BuildContext context) => BlocProvider(
|
|
|
|
|
create: (context) =>
|
|
|
|
|
getIt<CustomerLoaderBloc>()
|
|
|
|
|
..add(CustomerLoaderEvent.fetched(isRefresh: true)),
|
|
|
|
|
child: this,
|
|
|
|
|
);
|
2025-08-15 16:31:29 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _CustomerPageState extends State<CustomerPage>
|
|
|
|
|
with TickerProviderStateMixin {
|
|
|
|
|
final TextEditingController _searchController = TextEditingController();
|
2025-08-18 00:30:17 +07:00
|
|
|
ScrollController _scrollController = ScrollController();
|
2025-08-15 16:31:29 +07:00
|
|
|
bool _isGridView = false;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_searchController.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Scaffold(
|
|
|
|
|
backgroundColor: AppColor.background,
|
2025-08-18 00:30:17 +07:00
|
|
|
body: BlocBuilder<CustomerLoaderBloc, CustomerLoaderState>(
|
|
|
|
|
builder: (context, state) {
|
|
|
|
|
return NotificationListener<ScrollNotification>(
|
|
|
|
|
onNotification: (notification) {
|
|
|
|
|
if (notification is ScrollEndNotification &&
|
|
|
|
|
_scrollController.position.extentAfter == 0) {
|
|
|
|
|
context.read<CustomerLoaderBloc>().add(
|
|
|
|
|
CustomerLoaderEvent.fetched(),
|
|
|
|
|
);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
child: CustomScrollView(
|
|
|
|
|
controller: _scrollController,
|
|
|
|
|
slivers: [
|
|
|
|
|
// SliverAppBar with gradient
|
|
|
|
|
SliverAppBar(
|
|
|
|
|
expandedHeight: 120.0,
|
|
|
|
|
floating: false,
|
|
|
|
|
pinned: true,
|
|
|
|
|
backgroundColor: AppColor.primary,
|
|
|
|
|
flexibleSpace: CustomAppBar(title: 'Pelanggan'),
|
|
|
|
|
actions: [
|
|
|
|
|
ActionIconButton(onTap: () {}, icon: LineIcons.search),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Search and Filter Section
|
|
|
|
|
SliverToBoxAdapter(
|
|
|
|
|
child: Container(
|
|
|
|
|
color: AppColor.white,
|
|
|
|
|
child: Column(
|
2025-08-15 16:31:29 +07:00
|
|
|
children: [
|
2025-08-18 00:30:17 +07:00
|
|
|
// View toggle and sort
|
|
|
|
|
Padding(
|
|
|
|
|
padding: EdgeInsets.only(
|
|
|
|
|
left: 16,
|
|
|
|
|
right: 16,
|
|
|
|
|
bottom: 0,
|
2025-08-15 16:31:29 +07:00
|
|
|
),
|
2025-08-18 00:30:17 +07:00
|
|
|
child: Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
|
|
|
children: [
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
IconButton(
|
|
|
|
|
icon: Icon(
|
|
|
|
|
_isGridView
|
|
|
|
|
? Icons.list
|
|
|
|
|
: Icons.grid_view,
|
|
|
|
|
color: AppColor.primary,
|
|
|
|
|
),
|
|
|
|
|
onPressed: () {
|
|
|
|
|
setState(() {
|
|
|
|
|
_isGridView = !_isGridView;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
2025-08-15 16:31:29 +07:00
|
|
|
),
|
2025-08-18 00:30:17 +07:00
|
|
|
],
|
|
|
|
|
),
|
2025-08-15 16:31:29 +07:00
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
2025-08-18 00:30:17 +07:00
|
|
|
),
|
2025-08-15 16:31:29 +07:00
|
|
|
|
2025-08-18 00:30:17 +07:00
|
|
|
// Customer List
|
|
|
|
|
_isGridView
|
|
|
|
|
? _buildCustomerGrid(state.customers)
|
|
|
|
|
: _buildCustomerList(state.customers),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
2025-08-15 16:31:29 +07:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-18 00:30:17 +07:00
|
|
|
Widget _buildCustomerList(List<Customer> customers) {
|
2025-08-15 16:31:29 +07:00
|
|
|
return SliverList(
|
|
|
|
|
delegate: SliverChildBuilderDelegate((context, index) {
|
2025-08-18 00:30:17 +07:00
|
|
|
final customer = customers[index];
|
2025-08-15 16:31:29 +07:00
|
|
|
return CustomerTile(customer: customer);
|
2025-08-18 00:30:17 +07:00
|
|
|
}, childCount: customers.length),
|
2025-08-15 16:31:29 +07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-18 00:30:17 +07:00
|
|
|
Widget _buildCustomerGrid(List<Customer> customers) {
|
2025-08-15 16:31:29 +07:00
|
|
|
return SliverPadding(
|
|
|
|
|
padding: EdgeInsets.all(16),
|
|
|
|
|
sliver: SliverGrid(
|
|
|
|
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
|
|
|
crossAxisCount: 2,
|
|
|
|
|
crossAxisSpacing: 16,
|
|
|
|
|
mainAxisSpacing: 16,
|
|
|
|
|
childAspectRatio: 0.8,
|
|
|
|
|
),
|
|
|
|
|
delegate: SliverChildBuilderDelegate((context, index) {
|
2025-08-18 00:30:17 +07:00
|
|
|
final customer = customers[index];
|
2025-08-15 16:31:29 +07:00
|
|
|
return CustomerCard(customer: customer);
|
2025-08-18 00:30:17 +07:00
|
|
|
}, childCount: customers.length),
|
2025-08-15 16:31:29 +07:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|