feat: customer page
This commit is contained in:
@@ -1,127 +1,378 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../common/theme/theme.dart';
|
||||
import '../../../../domain/customer/customer.dart';
|
||||
import '../../../components/spacer/spacer.dart';
|
||||
import '../customer_page.dart';
|
||||
|
||||
class CustomerTile extends StatelessWidget {
|
||||
final Customer customer;
|
||||
const CustomerTile({super.key, required this.customer});
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onEdit;
|
||||
final VoidCallback? onDelete;
|
||||
|
||||
const CustomerTile({
|
||||
super.key,
|
||||
required this.customer,
|
||||
this.onTap,
|
||||
this.onEdit,
|
||||
this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: AppValue.margin, vertical: 6),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 2),
|
||||
color: Colors.black.withOpacity(0.06),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4),
|
||||
spreadRadius: -2,
|
||||
),
|
||||
],
|
||||
border: Border.all(
|
||||
color: customer.isActive
|
||||
? AppColor.primary.withOpacity(0.15)
|
||||
: Colors.grey.withOpacity(0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
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,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header Row
|
||||
Row(
|
||||
children: [
|
||||
// Avatar
|
||||
Stack(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 26,
|
||||
backgroundColor: _getAvatarColor(customer.name),
|
||||
child: Text(
|
||||
customer.name.isNotEmpty
|
||||
? customer.name[0].toUpperCase()
|
||||
: '?',
|
||||
style: AppStyle.lg.copyWith(
|
||||
color: AppColor.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Status indicator
|
||||
Positioned(
|
||||
bottom: 2,
|
||||
right: 2,
|
||||
child: Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: customer.isActive
|
||||
? AppColor.success
|
||||
: AppColor.error,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AppColor.white,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SpaceWidth(16),
|
||||
|
||||
// Customer Info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Name with badges
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
customer.name.isNotEmpty
|
||||
? customer.name
|
||||
: 'Unknown Customer',
|
||||
style: AppStyle.lg.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (customer.isDefault)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'DEFAULT',
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SpaceHeight(4),
|
||||
|
||||
// Contact info
|
||||
if (customer.email.isNotEmpty) ...[
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.email_outlined,
|
||||
size: 16,
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
const SpaceWidth(6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
customer.email,
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SpaceHeight(4),
|
||||
],
|
||||
|
||||
if (customer.phone.isNotEmpty)
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.phone_outlined,
|
||||
size: 16,
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
const SpaceWidth(6),
|
||||
Text(
|
||||
customer.phone,
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Address section
|
||||
if (customer.address.isNotEmpty) ...[
|
||||
const SpaceHeight(16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.background,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.location_on_outlined,
|
||||
size: 18,
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
const SpaceWidth(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
customer.address,
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// Footer with status and dates
|
||||
const SpaceHeight(16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Status badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: customer.isActive
|
||||
? AppColor.success.withOpacity(0.1)
|
||||
: AppColor.error.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: customer.isActive
|
||||
? AppColor.success
|
||||
: AppColor.error,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SpaceWidth(6),
|
||||
Text(
|
||||
customer.isActive ? 'Active' : 'Inactive',
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: customer.isActive
|
||||
? AppColor.success
|
||||
: AppColor.error,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Created date
|
||||
if (customer.createdAt.isNotEmpty)
|
||||
Text(
|
||||
'Joined ${_formatDate(customer.createdAt)}',
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Metadata section (if has any relevant data)
|
||||
if (customer.metadata.isNotEmpty && _hasRelevantMetadata()) ...[
|
||||
const SpaceHeight(12),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primary.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: AppColor.primary.withOpacity(0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 16,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
const SpaceWidth(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_getMetadataInfo(),
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
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;
|
||||
Color _getAvatarColor(String name) {
|
||||
final colors = [
|
||||
AppColor.primary,
|
||||
const Color(0xFF9C27B0),
|
||||
const Color(0xFFFF9800),
|
||||
const Color(0xFF607D8B),
|
||||
const Color(0xFF795548),
|
||||
const Color(0xFF4CAF50),
|
||||
const Color(0xFF2196F3),
|
||||
const Color(0xFFE91E63),
|
||||
];
|
||||
|
||||
final index = name.hashCode.abs() % colors.length;
|
||||
return colors[index];
|
||||
}
|
||||
|
||||
String _formatDate(String dateStr) {
|
||||
try {
|
||||
final date = DateTime.parse(dateStr);
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(date).inDays;
|
||||
|
||||
if (difference == 0) {
|
||||
return 'today';
|
||||
} else if (difference == 1) {
|
||||
return 'yesterday';
|
||||
} else if (difference < 30) {
|
||||
return '${difference}d ago';
|
||||
} else if (difference < 365) {
|
||||
final months = (difference / 30).floor();
|
||||
return '${months}mo ago';
|
||||
} else {
|
||||
final years = (difference / 365).floor();
|
||||
return '${years}y ago';
|
||||
}
|
||||
} catch (e) {
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
bool _hasRelevantMetadata() {
|
||||
return customer.metadata.containsKey('notes') ||
|
||||
customer.metadata.containsKey('tags') ||
|
||||
customer.metadata.containsKey('source') ||
|
||||
customer.metadata.containsKey('preferences');
|
||||
}
|
||||
|
||||
String _getMetadataInfo() {
|
||||
final info = <String>[];
|
||||
|
||||
if (customer.metadata.containsKey('notes')) {
|
||||
info.add('Has notes');
|
||||
}
|
||||
if (customer.metadata.containsKey('tags')) {
|
||||
final tags = customer.metadata['tags'];
|
||||
if (tags is List && tags.isNotEmpty) {
|
||||
info.add('${tags.length} tags');
|
||||
}
|
||||
}
|
||||
if (customer.metadata.containsKey('source')) {
|
||||
info.add('Source: ${customer.metadata['source']}');
|
||||
}
|
||||
|
||||
return info.join(' • ');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user