2025-08-15 16:31:29 +07:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import '../../../../common/theme/theme.dart';
|
2025-08-18 00:30:17 +07:00
|
|
|
import '../../../../domain/customer/customer.dart';
|
2025-08-15 16:31:29 +07:00
|
|
|
import '../../../components/spacer/spacer.dart';
|
|
|
|
|
|
|
|
|
|
class CustomerCard extends StatelessWidget {
|
|
|
|
|
final Customer customer;
|
2025-08-18 00:30:17 +07:00
|
|
|
final VoidCallback? onTap;
|
|
|
|
|
final VoidCallback? onLongPress;
|
|
|
|
|
|
|
|
|
|
const CustomerCard({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.customer,
|
|
|
|
|
this.onTap,
|
|
|
|
|
this.onLongPress,
|
|
|
|
|
});
|
2025-08-15 16:31:29 +07:00
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Container(
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: AppColor.white,
|
2025-08-18 00:30:17 +07:00
|
|
|
borderRadius: BorderRadius.circular(20),
|
2025-08-15 16:31:29 +07:00
|
|
|
boxShadow: [
|
|
|
|
|
BoxShadow(
|
2025-08-18 00:30:17 +07:00
|
|
|
color: Colors.black.withOpacity(0.06),
|
|
|
|
|
blurRadius: 20,
|
|
|
|
|
offset: const Offset(0, 6),
|
|
|
|
|
spreadRadius: -4,
|
2025-08-15 16:31:29 +07:00
|
|
|
),
|
|
|
|
|
],
|
2025-08-18 00:30:17 +07:00
|
|
|
border: Border.all(
|
|
|
|
|
color: customer.isActive
|
|
|
|
|
? AppColor.primary.withOpacity(0.1)
|
|
|
|
|
: Colors.grey.withOpacity(0.08),
|
|
|
|
|
width: 1.5,
|
|
|
|
|
),
|
2025-08-15 16:31:29 +07:00
|
|
|
),
|
2025-08-18 00:30:17 +07:00
|
|
|
child: Material(
|
|
|
|
|
color: Colors.transparent,
|
|
|
|
|
child: InkWell(
|
|
|
|
|
onTap: onTap,
|
|
|
|
|
onLongPress: onLongPress,
|
|
|
|
|
borderRadius: BorderRadius.circular(20),
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: const EdgeInsets.all(20),
|
|
|
|
|
child: Column(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
// Avatar with status indicator
|
|
|
|
|
Stack(
|
|
|
|
|
children: [
|
|
|
|
|
Container(
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
gradient: LinearGradient(
|
|
|
|
|
begin: Alignment.topLeft,
|
|
|
|
|
end: Alignment.bottomRight,
|
|
|
|
|
colors: [
|
|
|
|
|
_getAvatarColor(customer.name),
|
|
|
|
|
_getAvatarColor(customer.name).withOpacity(0.8),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
boxShadow: [
|
|
|
|
|
BoxShadow(
|
|
|
|
|
color: _getAvatarColor(
|
|
|
|
|
customer.name,
|
|
|
|
|
).withOpacity(0.3),
|
|
|
|
|
blurRadius: 12,
|
|
|
|
|
offset: const Offset(0, 4),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
child: CircleAvatar(
|
|
|
|
|
backgroundColor: Colors.transparent,
|
|
|
|
|
radius: 32,
|
|
|
|
|
child: Text(
|
|
|
|
|
customer.name.isNotEmpty
|
|
|
|
|
? customer.name[0].toUpperCase()
|
|
|
|
|
: '?',
|
|
|
|
|
style: AppStyle.xxl.copyWith(
|
|
|
|
|
color: AppColor.white,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Status indicator
|
|
|
|
|
Positioned(
|
|
|
|
|
bottom: 2,
|
|
|
|
|
right: 2,
|
|
|
|
|
child: Container(
|
|
|
|
|
width: 20,
|
|
|
|
|
height: 20,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: customer.isActive
|
|
|
|
|
? AppColor.success
|
|
|
|
|
: AppColor.error,
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
border: Border.all(color: AppColor.white, width: 3),
|
|
|
|
|
boxShadow: [
|
|
|
|
|
BoxShadow(
|
|
|
|
|
color: Colors.black.withOpacity(0.1),
|
|
|
|
|
blurRadius: 4,
|
|
|
|
|
offset: const Offset(0, 2),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Default badge
|
|
|
|
|
if (customer.isDefault)
|
|
|
|
|
Positioned(
|
|
|
|
|
top: -2,
|
|
|
|
|
left: -8,
|
|
|
|
|
child: Container(
|
|
|
|
|
padding: const EdgeInsets.symmetric(
|
|
|
|
|
horizontal: 6,
|
|
|
|
|
vertical: 2,
|
|
|
|
|
),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: AppColor.primary,
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
boxShadow: [
|
|
|
|
|
BoxShadow(
|
|
|
|
|
color: AppColor.primary.withOpacity(0.3),
|
|
|
|
|
blurRadius: 4,
|
|
|
|
|
offset: const Offset(0, 2),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
child: Text(
|
|
|
|
|
'⭐',
|
|
|
|
|
style: AppStyle.xs.copyWith(
|
|
|
|
|
color: AppColor.white,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
const SpaceHeight(16),
|
|
|
|
|
|
|
|
|
|
// Customer Name
|
|
|
|
|
Text(
|
|
|
|
|
customer.name.isNotEmpty ? customer.name : 'Unknown Customer',
|
|
|
|
|
style: AppStyle.lg.copyWith(
|
2025-08-15 16:31:29 +07:00
|
|
|
fontWeight: FontWeight.bold,
|
2025-08-18 00:30:17 +07:00
|
|
|
color: AppColor.textPrimary,
|
2025-08-15 16:31:29 +07:00
|
|
|
),
|
2025-08-18 00:30:17 +07:00
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
maxLines: 2,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
2025-08-15 16:31:29 +07:00
|
|
|
),
|
2025-08-18 00:30:17 +07:00
|
|
|
|
|
|
|
|
const SpaceHeight(8),
|
|
|
|
|
|
|
|
|
|
// Contact Info
|
|
|
|
|
if (customer.email.isNotEmpty || customer.phone.isNotEmpty) ...[
|
|
|
|
|
Column(
|
|
|
|
|
children: [
|
|
|
|
|
if (customer.email.isNotEmpty)
|
|
|
|
|
_buildContactInfo(Icons.email_outlined, customer.email),
|
|
|
|
|
if (customer.email.isNotEmpty &&
|
|
|
|
|
customer.phone.isNotEmpty)
|
|
|
|
|
const SpaceHeight(4),
|
|
|
|
|
if (customer.phone.isNotEmpty)
|
|
|
|
|
_buildContactInfo(Icons.phone_outlined, customer.phone),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SpaceHeight(12),
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
// 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(16),
|
|
|
|
|
border: Border.all(
|
|
|
|
|
color: customer.isActive
|
|
|
|
|
? AppColor.success.withOpacity(0.3)
|
|
|
|
|
: AppColor.error.withOpacity(0.3),
|
|
|
|
|
width: 1,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
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,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
2025-08-15 16:31:29 +07:00
|
|
|
),
|
|
|
|
|
),
|
2025-08-18 00:30:17 +07:00
|
|
|
|
|
|
|
|
// Additional info if available
|
|
|
|
|
if (customer.address.isNotEmpty) ...[
|
|
|
|
|
const SpaceHeight(8),
|
|
|
|
|
Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
Icon(
|
|
|
|
|
Icons.location_on_outlined,
|
|
|
|
|
size: 14,
|
|
|
|
|
color: AppColor.textSecondary,
|
|
|
|
|
),
|
|
|
|
|
const SpaceWidth(4),
|
|
|
|
|
Flexible(
|
|
|
|
|
child: Text(
|
|
|
|
|
customer.address,
|
|
|
|
|
style: AppStyle.xs.copyWith(
|
|
|
|
|
color: AppColor.textSecondary,
|
|
|
|
|
),
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
maxLines: 1,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
// Metadata info
|
|
|
|
|
if (customer.metadata.isNotEmpty && _hasRelevantMetadata()) ...[
|
|
|
|
|
const SpaceHeight(8),
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.symmetric(
|
|
|
|
|
horizontal: 8,
|
|
|
|
|
vertical: 4,
|
|
|
|
|
),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: AppColor.primary.withOpacity(0.1),
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
),
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
Icon(
|
|
|
|
|
Icons.star_outline,
|
|
|
|
|
size: 12,
|
|
|
|
|
color: AppColor.primary,
|
|
|
|
|
),
|
|
|
|
|
const SpaceWidth(4),
|
|
|
|
|
Text(
|
|
|
|
|
_getMetadataInfo(),
|
|
|
|
|
style: AppStyle.xs.copyWith(
|
|
|
|
|
color: AppColor.primary,
|
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
// Join date
|
|
|
|
|
if (customer.createdAt.isNotEmpty) ...[
|
|
|
|
|
const SpaceHeight(8),
|
|
|
|
|
Text(
|
|
|
|
|
'Joined ${_formatDate(customer.createdAt)}',
|
|
|
|
|
style: AppStyle.xs.copyWith(color: AppColor.textSecondary),
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
),
|
2025-08-15 16:31:29 +07:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-18 00:30:17 +07:00
|
|
|
Widget _buildContactInfo(IconData icon, String text) {
|
|
|
|
|
return Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
Icon(icon, size: 14, color: AppColor.textSecondary),
|
|
|
|
|
const SpaceWidth(6),
|
|
|
|
|
Flexible(
|
|
|
|
|
child: Text(
|
|
|
|
|
text,
|
|
|
|
|
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
maxLines: 1,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Color _getAvatarColor(String name) {
|
|
|
|
|
final colors = [
|
|
|
|
|
AppColor.primary,
|
|
|
|
|
const Color(0xFF9C27B0), // Purple
|
|
|
|
|
const Color(0xFFFF9800), // Orange
|
|
|
|
|
const Color(0xFF607D8B), // Blue Grey
|
|
|
|
|
const Color(0xFF795548), // Brown
|
|
|
|
|
const Color(0xFF4CAF50), // Green
|
|
|
|
|
const Color(0xFF2196F3), // Blue
|
|
|
|
|
const Color(0xFFE91E63), // Pink
|
|
|
|
|
const Color(0xFF00BCD4), // Cyan
|
|
|
|
|
const Color(0xFFFF5722), // Deep Orange
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if (name.isEmpty) return AppColor.primary;
|
|
|
|
|
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') ||
|
|
|
|
|
customer.metadata.containsKey('vip') ||
|
|
|
|
|
customer.metadata.containsKey('tier');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String _getMetadataInfo() {
|
|
|
|
|
if (customer.metadata.containsKey('vip') &&
|
|
|
|
|
customer.metadata['vip'] == true) {
|
|
|
|
|
return 'VIP';
|
|
|
|
|
}
|
|
|
|
|
if (customer.metadata.containsKey('tier')) {
|
|
|
|
|
return customer.metadata['tier'].toString();
|
|
|
|
|
}
|
|
|
|
|
if (customer.metadata.containsKey('tags')) {
|
|
|
|
|
final tags = customer.metadata['tags'];
|
|
|
|
|
if (tags is List && tags.isNotEmpty) {
|
|
|
|
|
return tags.first.toString();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (customer.metadata.containsKey('source')) {
|
|
|
|
|
return customer.metadata['source'].toString();
|
2025-08-15 16:31:29 +07:00
|
|
|
}
|
2025-08-18 00:30:17 +07:00
|
|
|
return 'Special';
|
2025-08-15 16:31:29 +07:00
|
|
|
}
|
|
|
|
|
}
|