feat: customer page

This commit is contained in:
efrilm 2025-08-15 16:31:29 +07:00
parent c9e5c8d178
commit 8c2afc4880
8 changed files with 643 additions and 75 deletions

View File

@ -0,0 +1,235 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:line_icons/line_icons.dart';
import 'dart:math' as math;
import '../../../common/theme/theme.dart';
import '../../components/button/button.dart';
import 'widgets/appbar.dart';
import 'widgets/customer_card.dart';
import 'widgets/customer_tile.dart';
// Customer Model
class Customer {
final String id;
final String name;
final String email;
final String phone;
final String address;
final double totalPurchases;
final int totalOrders;
final DateTime lastVisit;
final String membershipLevel;
final bool isActive;
Customer({
required this.id,
required this.name,
required this.email,
required this.phone,
required this.address,
required this.totalPurchases,
required this.totalOrders,
required this.lastVisit,
required this.membershipLevel,
required this.isActive,
});
}
@RoutePage()
class CustomerPage extends StatefulWidget {
const CustomerPage({super.key});
@override
State<CustomerPage> createState() => _CustomerPageState();
}
class _CustomerPageState extends State<CustomerPage>
with TickerProviderStateMixin {
final TextEditingController _searchController = TextEditingController();
String _searchQuery = '';
bool _isGridView = false;
// Sample customer data
final List<Customer> _customers = [
Customer(
id: '001',
name: 'Ahmad Wijaya',
email: 'ahmad@email.com',
phone: '+62 812-3456-7890',
address: 'Jl. Raya No. 123, Jakarta',
totalPurchases: 2500000,
totalOrders: 15,
lastVisit: DateTime.now().subtract(Duration(days: 2)),
membershipLevel: 'Gold',
isActive: true,
),
Customer(
id: '002',
name: 'Siti Nurhaliza',
email: 'siti@email.com',
phone: '+62 813-4567-8901',
address: 'Jl. Merdeka No. 45, Bandung',
totalPurchases: 1800000,
totalOrders: 12,
lastVisit: DateTime.now().subtract(Duration(days: 5)),
membershipLevel: 'Silver',
isActive: true,
),
Customer(
id: '003',
name: 'Budi Santoso',
email: 'budi@email.com',
phone: '+62 814-5678-9012',
address: 'Jl. Sudirman No. 67, Surabaya',
totalPurchases: 3200000,
totalOrders: 20,
lastVisit: DateTime.now().subtract(Duration(days: 1)),
membershipLevel: 'Platinum',
isActive: true,
),
Customer(
id: '004',
name: 'Maya Sari',
email: 'maya@email.com',
phone: '+62 815-6789-0123',
address: 'Jl. Diponegoro No. 89, Yogyakarta',
totalPurchases: 950000,
totalOrders: 8,
lastVisit: DateTime.now().subtract(Duration(days: 30)),
membershipLevel: 'Bronze',
isActive: false,
),
];
// Animation
late AnimationController _rotationController;
late Animation<double> _rotationAnimation;
@override
initState() {
super.initState();
_rotationController = AnimationController(
duration: const Duration(seconds: 3),
vsync: this,
)..repeat();
_rotationAnimation = Tween<double>(
begin: 0,
end: 2 * math.pi,
).animate(_rotationController);
}
@override
void dispose() {
_searchController.dispose();
_rotationController.dispose();
super.dispose();
}
List<Customer> get filteredCustomers {
var filtered = _customers.where((customer) {
final matchesSearch =
customer.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
customer.email.toLowerCase().contains(_searchQuery.toLowerCase()) ||
customer.phone.contains(_searchQuery);
return matchesSearch;
}).toList();
return filtered;
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColor.background,
body: CustomScrollView(
slivers: [
// SliverAppBar with gradient
SliverAppBar(
expandedHeight: 120.0,
floating: false,
pinned: true,
backgroundColor: AppColor.primary,
flexibleSpace: CustomerAppbar(
rotationAnimation: _rotationAnimation,
),
actions: [ActionIconButton(onTap: () {}, icon: LineIcons.search)],
),
// Search and Filter Section
SliverToBoxAdapter(
child: Container(
color: AppColor.white,
child: Column(
children: [
// View toggle and sort
Padding(
padding: EdgeInsets.only(left: 16, right: 16, bottom: 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${filteredCustomers.length} customers found',
style: TextStyle(
color: AppColor.textSecondary,
fontSize: 14,
),
),
Row(
children: [
IconButton(
icon: Icon(
_isGridView ? Icons.list : Icons.grid_view,
color: AppColor.primary,
),
onPressed: () {
setState(() {
_isGridView = !_isGridView;
});
},
),
],
),
],
),
),
],
),
),
),
// Customer List
_isGridView ? _buildCustomerGrid() : _buildCustomerList(),
],
),
);
}
Widget _buildCustomerList() {
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
final customer = filteredCustomers[index];
return CustomerTile(customer: customer);
}, childCount: filteredCustomers.length),
);
}
Widget _buildCustomerGrid() {
return SliverPadding(
padding: EdgeInsets.all(16),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 0.8,
),
delegate: SliverChildBuilderDelegate((context, index) {
final customer = filteredCustomers[index];
return CustomerCard(customer: customer);
}, childCount: filteredCustomers.length),
),
);
}
}

View File

@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import '../../../../common/theme/theme.dart';
class CustomerAppbar extends StatelessWidget {
final Animation<double> rotationAnimation;
const CustomerAppbar({super.key, required this.rotationAnimation});
@override
Widget build(BuildContext context) {
return FlexibleSpaceBar(
titlePadding: const EdgeInsets.only(left: 50, bottom: 16),
title: Text(
'Customer',
style: AppStyle.xl.copyWith(
color: AppColor.textWhite,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
background: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: AppColor.primaryGradient,
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Stack(
children: [
Positioned(
right: -20,
top: -20,
child: AnimatedBuilder(
animation: rotationAnimation,
builder: (context, child) {
return Transform.rotate(
angle: rotationAnimation.value,
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.white.withOpacity(0.1),
),
),
);
},
),
),
Positioned(
left: -30,
bottom: -30,
child: AnimatedBuilder(
animation: rotationAnimation,
builder: (context, child) {
return Transform.rotate(
angle: -rotationAnimation.value * 0.5,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.white.withOpacity(0.05),
),
),
);
},
),
),
Positioned(
right: 80,
bottom: 30,
child: AnimatedBuilder(
animation: rotationAnimation,
builder: (context, child) {
return Transform.rotate(
angle: -rotationAnimation.value * 0.2,
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: AppColor.white.withOpacity(0.08),
),
),
);
},
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/spacer/spacer.dart';
import '../customer_page.dart';
class CustomerCard extends StatelessWidget {
final Customer customer;
const CustomerCard({super.key, required this.customer});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: InkWell(
onTap: () {},
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
CircleAvatar(
backgroundColor: _getMembershipColor(customer.membershipLevel),
radius: 30,
child: Text(
customer.name[0].toUpperCase(),
style: AppStyle.xxl.copyWith(
color: AppColor.white,
fontWeight: FontWeight.bold,
),
),
),
SpaceHeight(12),
Text(
customer.name,
style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 8),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getMembershipColor(
customer.membershipLevel,
).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
customer.membershipLevel,
style: AppStyle.sm.copyWith(
color: _getMembershipColor(customer.membershipLevel),
fontWeight: FontWeight.bold,
),
),
),
],
),
),
),
);
}
Color _getMembershipColor(String level) {
switch (level) {
case 'Platinum':
return Color(0xFF9C27B0);
case 'Gold':
return Color(0xFFFF9800);
case 'Silver':
return Color(0xFF607D8B);
case 'Bronze':
return Color(0xFF795548);
default:
return AppColor.primary;
}
}
}

View File

@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/spacer/spacer.dart';
import '../customer_page.dart';
class CustomerTile extends StatelessWidget {
final Customer customer;
const CustomerTile({super.key, required this.customer});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: AppValue.margin, vertical: 6),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 2),
),
],
),
child: ListTile(
contentPadding: EdgeInsets.all(16),
leading: CircleAvatar(
backgroundColor: _getMembershipColor(customer.membershipLevel),
child: Text(
customer.name[0].toUpperCase(),
style: AppStyle.sm.copyWith(
color: AppColor.white,
fontWeight: FontWeight.bold,
),
),
),
title: Text(
customer.name,
style: AppStyle.lg.copyWith(fontWeight: FontWeight.bold),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SpaceHeight(4),
Text(customer.email),
SpaceHeight(2),
Text(customer.phone),
SpaceHeight(4),
Row(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getMembershipColor(
customer.membershipLevel,
).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
customer.membershipLevel,
style: AppStyle.sm.copyWith(
color: _getMembershipColor(customer.membershipLevel),
fontWeight: FontWeight.bold,
),
),
),
SpaceWidth(8),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: customer.isActive
? AppColor.success.withOpacity(0.1)
: AppColor.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
customer.isActive ? 'Active' : 'Inactive',
style: AppStyle.sm.copyWith(
color: customer.isActive
? AppColor.success
: AppColor.error,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'Rp ${customer.totalPurchases.toStringAsFixed(0)}',
style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.primary,
),
),
Text(
'${customer.totalOrders} orders',
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
),
],
),
onTap: () {},
),
);
}
Color _getMembershipColor(String level) {
switch (level) {
case 'Platinum':
return Color(0xFF9C27B0);
case 'Gold':
return Color(0xFFFF9800);
case 'Silver':
return Color(0xFF607D8B);
case 'Bronze':
return Color(0xFF795548);
default:
return AppColor.primary;
}
}
}

View File

@ -78,16 +78,16 @@ class HomeFeature extends StatelessWidget {
onTap: () => context.router.push(ScheduleRoute()),
),
HomeFeatureTile(
title: 'Aset Tetap',
title: 'Inventaris',
color: const Color(0xFF00BCD4),
icon: LineIcons.businessTime,
icon: LineIcons.archive,
onTap: () {},
),
HomeFeatureTile(
title: 'Kontak',
title: 'Pelanggan',
color: const Color(0xFFFF5722),
icon: LineIcons.userPlus,
onTap: () {},
onTap: () => context.router.push(CustomerRoute()),
),
],
),

View File

@ -23,7 +23,7 @@ class HomeFeatureTile extends StatelessWidget {
onTap: onTap,
borderRadius: BorderRadius.circular(AppValue.radius),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 6),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [

View File

@ -33,5 +33,8 @@ class AppRouter extends RootStackRouter {
// Product
AutoRoute(page: ProductRoute.page),
// Customer
AutoRoute(page: CustomerRoute.page),
];
}

View File

@ -10,201 +10,219 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:apskel_owner_flutter/presentation/pages/auth/login/login_page.dart'
as _i4;
import 'package:apskel_owner_flutter/presentation/pages/form/daily_task_form_page.dart'
as _i1;
import 'package:apskel_owner_flutter/presentation/pages/home/home_page.dart'
as _i2;
import 'package:apskel_owner_flutter/presentation/pages/language/language_page.dart'
as _i3;
import 'package:apskel_owner_flutter/presentation/pages/main/main_page.dart'
as _i5;
import 'package:apskel_owner_flutter/presentation/pages/product/product_page.dart'
import 'package:apskel_owner_flutter/presentation/pages/customer/customer_page.dart'
as _i1;
import 'package:apskel_owner_flutter/presentation/pages/form/daily_task_form_page.dart'
as _i2;
import 'package:apskel_owner_flutter/presentation/pages/home/home_page.dart'
as _i3;
import 'package:apskel_owner_flutter/presentation/pages/language/language_page.dart'
as _i4;
import 'package:apskel_owner_flutter/presentation/pages/main/main_page.dart'
as _i6;
import 'package:apskel_owner_flutter/presentation/pages/profile/profile_page.dart'
import 'package:apskel_owner_flutter/presentation/pages/product/product_page.dart'
as _i7;
import 'package:apskel_owner_flutter/presentation/pages/report/report_page.dart'
import 'package:apskel_owner_flutter/presentation/pages/profile/profile_page.dart'
as _i8;
import 'package:apskel_owner_flutter/presentation/pages/schedule/schedule_page.dart'
import 'package:apskel_owner_flutter/presentation/pages/report/report_page.dart'
as _i9;
import 'package:apskel_owner_flutter/presentation/pages/splash/splash_page.dart'
import 'package:apskel_owner_flutter/presentation/pages/schedule/schedule_page.dart'
as _i10;
import 'package:apskel_owner_flutter/presentation/pages/transaction/transaction_page.dart'
import 'package:apskel_owner_flutter/presentation/pages/splash/splash_page.dart'
as _i11;
import 'package:auto_route/auto_route.dart' as _i12;
import 'package:apskel_owner_flutter/presentation/pages/transaction/transaction_page.dart'
as _i12;
import 'package:auto_route/auto_route.dart' as _i13;
/// generated route for
/// [_i1.DailyTasksFormPage]
class DailyTasksFormRoute extends _i12.PageRouteInfo<void> {
const DailyTasksFormRoute({List<_i12.PageRouteInfo>? children})
/// [_i1.CustomerPage]
class CustomerRoute extends _i13.PageRouteInfo<void> {
const CustomerRoute({List<_i13.PageRouteInfo>? children})
: super(CustomerRoute.name, initialChildren: children);
static const String name = 'CustomerRoute';
static _i13.PageInfo page = _i13.PageInfo(
name,
builder: (data) {
return const _i1.CustomerPage();
},
);
}
/// generated route for
/// [_i2.DailyTasksFormPage]
class DailyTasksFormRoute extends _i13.PageRouteInfo<void> {
const DailyTasksFormRoute({List<_i13.PageRouteInfo>? children})
: super(DailyTasksFormRoute.name, initialChildren: children);
static const String name = 'DailyTasksFormRoute';
static _i12.PageInfo page = _i12.PageInfo(
static _i13.PageInfo page = _i13.PageInfo(
name,
builder: (data) {
return const _i1.DailyTasksFormPage();
return const _i2.DailyTasksFormPage();
},
);
}
/// generated route for
/// [_i2.HomePage]
class HomeRoute extends _i12.PageRouteInfo<void> {
const HomeRoute({List<_i12.PageRouteInfo>? children})
/// [_i3.HomePage]
class HomeRoute extends _i13.PageRouteInfo<void> {
const HomeRoute({List<_i13.PageRouteInfo>? children})
: super(HomeRoute.name, initialChildren: children);
static const String name = 'HomeRoute';
static _i12.PageInfo page = _i12.PageInfo(
static _i13.PageInfo page = _i13.PageInfo(
name,
builder: (data) {
return const _i2.HomePage();
return const _i3.HomePage();
},
);
}
/// generated route for
/// [_i3.LanguagePage]
class LanguageRoute extends _i12.PageRouteInfo<void> {
const LanguageRoute({List<_i12.PageRouteInfo>? children})
/// [_i4.LanguagePage]
class LanguageRoute extends _i13.PageRouteInfo<void> {
const LanguageRoute({List<_i13.PageRouteInfo>? children})
: super(LanguageRoute.name, initialChildren: children);
static const String name = 'LanguageRoute';
static _i12.PageInfo page = _i12.PageInfo(
static _i13.PageInfo page = _i13.PageInfo(
name,
builder: (data) {
return const _i3.LanguagePage();
return const _i4.LanguagePage();
},
);
}
/// generated route for
/// [_i4.LoginPage]
class LoginRoute extends _i12.PageRouteInfo<void> {
const LoginRoute({List<_i12.PageRouteInfo>? children})
/// [_i5.LoginPage]
class LoginRoute extends _i13.PageRouteInfo<void> {
const LoginRoute({List<_i13.PageRouteInfo>? children})
: super(LoginRoute.name, initialChildren: children);
static const String name = 'LoginRoute';
static _i12.PageInfo page = _i12.PageInfo(
static _i13.PageInfo page = _i13.PageInfo(
name,
builder: (data) {
return const _i4.LoginPage();
return const _i5.LoginPage();
},
);
}
/// generated route for
/// [_i5.MainPage]
class MainRoute extends _i12.PageRouteInfo<void> {
const MainRoute({List<_i12.PageRouteInfo>? children})
/// [_i6.MainPage]
class MainRoute extends _i13.PageRouteInfo<void> {
const MainRoute({List<_i13.PageRouteInfo>? children})
: super(MainRoute.name, initialChildren: children);
static const String name = 'MainRoute';
static _i12.PageInfo page = _i12.PageInfo(
static _i13.PageInfo page = _i13.PageInfo(
name,
builder: (data) {
return const _i5.MainPage();
return const _i6.MainPage();
},
);
}
/// generated route for
/// [_i6.ProductPage]
class ProductRoute extends _i12.PageRouteInfo<void> {
const ProductRoute({List<_i12.PageRouteInfo>? children})
/// [_i7.ProductPage]
class ProductRoute extends _i13.PageRouteInfo<void> {
const ProductRoute({List<_i13.PageRouteInfo>? children})
: super(ProductRoute.name, initialChildren: children);
static const String name = 'ProductRoute';
static _i12.PageInfo page = _i12.PageInfo(
static _i13.PageInfo page = _i13.PageInfo(
name,
builder: (data) {
return const _i6.ProductPage();
return const _i7.ProductPage();
},
);
}
/// generated route for
/// [_i7.ProfilePage]
class ProfileRoute extends _i12.PageRouteInfo<void> {
const ProfileRoute({List<_i12.PageRouteInfo>? children})
/// [_i8.ProfilePage]
class ProfileRoute extends _i13.PageRouteInfo<void> {
const ProfileRoute({List<_i13.PageRouteInfo>? children})
: super(ProfileRoute.name, initialChildren: children);
static const String name = 'ProfileRoute';
static _i12.PageInfo page = _i12.PageInfo(
static _i13.PageInfo page = _i13.PageInfo(
name,
builder: (data) {
return const _i7.ProfilePage();
return const _i8.ProfilePage();
},
);
}
/// generated route for
/// [_i8.ReportPage]
class ReportRoute extends _i12.PageRouteInfo<void> {
const ReportRoute({List<_i12.PageRouteInfo>? children})
/// [_i9.ReportPage]
class ReportRoute extends _i13.PageRouteInfo<void> {
const ReportRoute({List<_i13.PageRouteInfo>? children})
: super(ReportRoute.name, initialChildren: children);
static const String name = 'ReportRoute';
static _i12.PageInfo page = _i12.PageInfo(
static _i13.PageInfo page = _i13.PageInfo(
name,
builder: (data) {
return const _i8.ReportPage();
return const _i9.ReportPage();
},
);
}
/// generated route for
/// [_i9.SchedulePage]
class ScheduleRoute extends _i12.PageRouteInfo<void> {
const ScheduleRoute({List<_i12.PageRouteInfo>? children})
/// [_i10.SchedulePage]
class ScheduleRoute extends _i13.PageRouteInfo<void> {
const ScheduleRoute({List<_i13.PageRouteInfo>? children})
: super(ScheduleRoute.name, initialChildren: children);
static const String name = 'ScheduleRoute';
static _i12.PageInfo page = _i12.PageInfo(
static _i13.PageInfo page = _i13.PageInfo(
name,
builder: (data) {
return const _i9.SchedulePage();
return const _i10.SchedulePage();
},
);
}
/// generated route for
/// [_i10.SplashPage]
class SplashRoute extends _i12.PageRouteInfo<void> {
const SplashRoute({List<_i12.PageRouteInfo>? children})
/// [_i11.SplashPage]
class SplashRoute extends _i13.PageRouteInfo<void> {
const SplashRoute({List<_i13.PageRouteInfo>? children})
: super(SplashRoute.name, initialChildren: children);
static const String name = 'SplashRoute';
static _i12.PageInfo page = _i12.PageInfo(
static _i13.PageInfo page = _i13.PageInfo(
name,
builder: (data) {
return const _i10.SplashPage();
return const _i11.SplashPage();
},
);
}
/// generated route for
/// [_i11.TransactionPage]
class TransactionRoute extends _i12.PageRouteInfo<void> {
const TransactionRoute({List<_i12.PageRouteInfo>? children})
/// [_i12.TransactionPage]
class TransactionRoute extends _i13.PageRouteInfo<void> {
const TransactionRoute({List<_i13.PageRouteInfo>? children})
: super(TransactionRoute.name, initialChildren: children);
static const String name = 'TransactionRoute';
static _i12.PageInfo page = _i12.PageInfo(
static _i13.PageInfo page = _i13.PageInfo(
name,
builder: (data) {
return const _i11.TransactionPage();
return const _i12.TransactionPage();
},
);
}