it0/it0_app/lib/features/referral/presentation/screens/referral_screen.dart

1183 lines
39 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:it0_app/l10n/app_localizations.dart';
import '../../../../core/theme/app_colors.dart';
import '../providers/referral_providers.dart';
import '../../domain/models/referral_info.dart';
class ReferralScreen extends ConsumerWidget {
const ReferralScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final isDark = Theme.of(context).brightness == Brightness.dark;
final cardColor = isDark ? AppColors.surface : Colors.white;
return DefaultTabController(
length: 2,
child: Scaffold(
backgroundColor: AppColors.background,
appBar: AppBar(
backgroundColor: AppColors.background,
title: Text(l10n.referralScreenTitle),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
ref.invalidate(referralInfoProvider);
ref.invalidate(referralListProvider);
ref.invalidate(pendingRewardsProvider);
ref.invalidate(userReferralInfoProvider);
ref.invalidate(myCircleProvider);
ref.invalidate(myPointsProvider);
},
),
],
bottom: TabBar(
indicatorColor: AppColors.primary,
labelColor: AppColors.primary,
unselectedLabelColor: Colors.grey,
tabs: [
Tab(text: l10n.referralTabTenant),
Tab(text: l10n.referralTabPersonal),
],
),
),
body: TabBarView(
children: [
_TenantReferralTab(cardColor: cardColor),
_PersonalCircleTab(cardColor: cardColor),
],
),
),
);
}
}
// ══════════════════════════════════════════════════════════════════════════════
// Tab 1 — Tenant / B2B referral (existing)
// ══════════════════════════════════════════════════════════════════════════════
class _TenantReferralTab extends ConsumerWidget {
final Color cardColor;
const _TenantReferralTab({required this.cardColor});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final infoAsync = ref.watch(referralInfoProvider);
return infoAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('${l10n.loadFailed}: $e')),
data: (info) => ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
children: [
_ReferralCodeCard(info: info, cardColor: cardColor),
const SizedBox(height: 16),
_StatsRow(info: info, cardColor: cardColor),
const SizedBox(height: 20),
_RewardRulesCard(cardColor: cardColor),
const SizedBox(height: 20),
_SectionHeader(
title: l10n.referralRecordsSection,
onTap: () => Navigator.push(context,
MaterialPageRoute(builder: (_) => const _ReferralListPage())),
),
_ReferralPreviewList(cardColor: cardColor),
const SizedBox(height: 20),
_SectionHeader(
title: l10n.pendingRewardsSection,
onTap: () => Navigator.push(context,
MaterialPageRoute(builder: (_) => const _RewardListPage())),
),
_RewardPreviewList(cardColor: cardColor),
const SizedBox(height: 40),
],
),
);
}
}
// ══════════════════════════════════════════════════════════════════════════════
// Tab 2 — Personal circle / C2C
// ══════════════════════════════════════════════════════════════════════════════
class _PersonalCircleTab extends ConsumerWidget {
final Color cardColor;
const _PersonalCircleTab({required this.cardColor});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final infoAsync = ref.watch(userReferralInfoProvider);
return infoAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('${l10n.loadFailed}: $e')),
data: (info) => ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
children: [
_UserCodeCard(info: info, cardColor: cardColor),
const SizedBox(height: 16),
_PointsBalanceCard(info: info, cardColor: cardColor),
const SizedBox(height: 20),
_CircleRulesCard(cardColor: cardColor),
const SizedBox(height: 20),
_SectionHeader(
title: l10n.myCircleMembersSection,
onTap: () => Navigator.push(context,
MaterialPageRoute(builder: (_) => const _CircleListPage())),
),
_CirclePreviewList(cardColor: cardColor),
const SizedBox(height: 20),
_SectionHeader(
title: l10n.pointsHistorySection,
onTap: () => Navigator.push(context,
MaterialPageRoute(builder: (_) => const _PointsHistoryPage())),
),
_PointsPreviewList(cardColor: cardColor),
const SizedBox(height: 40),
],
),
);
}
}
// ── User Referral Code Card ───────────────────────────────────────────────────
class _UserCodeCard extends StatelessWidget {
final UserReferralInfo info;
final Color cardColor;
const _UserCodeCard({required this.info, required this.cardColor});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Card(
color: cardColor,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.myPersonalInviteCode,
style: const TextStyle(color: Colors.grey, fontSize: 13),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: Text(
info.code,
style: const TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
letterSpacing: 2,
color: Color(0xFF7C3AED),
),
),
),
IconButton(
icon: const Icon(Icons.copy, color: Color(0xFF7C3AED)),
tooltip: l10n.copyReferralCodeTooltip,
onPressed: () => _copy(context, info.code),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
icon: const Icon(Icons.link, size: 18),
label: Text(l10n.copyInviteLinkButton),
onPressed: () => _copy(context, info.shareUrl),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF7C3AED),
side: const BorderSide(color: Color(0xFF7C3AED)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
),
),
const SizedBox(width: 8),
Expanded(
child: FilledButton.icon(
icon: const Icon(Icons.share, size: 18),
label: Text(l10n.shareButton),
onPressed: () => _share(context, info),
style: FilledButton.styleFrom(
backgroundColor: const Color(0xFF7C3AED),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
),
),
],
),
],
),
),
);
}
void _copy(BuildContext context, String text) {
final l10n = AppLocalizations.of(context);
Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.copiedToClipboard),
duration: const Duration(seconds: 2)),
);
}
void _share(BuildContext context, UserReferralInfo info) {
final text =
'邀请你加入 IT0 智能体管理平台用AI管理你的数字工作加入即获 200 积分奖励!\n邀请码:${info.code}\n链接:${info.shareUrl}';
_copy(context, text);
}
}
// ── Points Balance Card ───────────────────────────────────────────────────────
class _PointsBalanceCard extends StatelessWidget {
final UserReferralInfo info;
final Color cardColor;
const _PointsBalanceCard({required this.info, required this.cardColor});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Card(
color: cardColor,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.stars_rounded, color: Color(0xFFF59E0B), size: 20),
const SizedBox(width: 6),
Text(l10n.pointsBalanceTitle,
style:
const TextStyle(fontWeight: FontWeight.w600, fontSize: 15)),
],
),
const SizedBox(height: 16),
Row(
children: [
_PointsStatItem(
label: l10n.currentBalanceLabel,
value: '${info.pointsBalance}',
unit: 'pts',
color: const Color(0xFF7C3AED),
),
const SizedBox(width: 12),
_PointsStatItem(
label: l10n.circleMembersCountLabel,
value: '${info.circleSize}',
unit: '',
color: const Color(0xFF10B981),
),
const SizedBox(width: 12),
_PointsStatItem(
label: l10n.totalEarnedLabel,
value: '${info.totalEarned}',
unit: 'pts',
color: const Color(0xFF6366F1),
),
],
),
],
),
),
);
}
}
class _PointsStatItem extends StatelessWidget {
final String label;
final String value;
final String unit;
final Color color;
const _PointsStatItem({
required this.label,
required this.value,
required this.unit,
required this.color,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)),
const SizedBox(height: 4),
RichText(
text: TextSpan(
children: [
TextSpan(
text: value,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: color,
),
),
TextSpan(
text: ' $unit',
style: TextStyle(fontSize: 12, color: color.withAlpha(180)),
),
],
),
),
],
),
);
}
}
// ── Circle Rules Card ─────────────────────────────────────────────────────────
class _CircleRulesCard extends StatelessWidget {
final Color cardColor;
const _CircleRulesCard({required this.cardColor});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Card(
color: cardColor,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.people_alt_rounded,
color: Color(0xFF7C3AED), size: 20),
const SizedBox(width: 6),
Text(l10n.circleRewardRulesTitle,
style:
const TextStyle(fontWeight: FontWeight.w600, fontSize: 15)),
],
),
const SizedBox(height: 12),
_RuleItem(
icon: Icons.card_giftcard_rounded,
color: const Color(0xFF7C3AED),
text: l10n.circleRule1,
),
const SizedBox(height: 8),
_RuleItem(
icon: Icons.star_rounded,
color: const Color(0xFF6366F1),
text: l10n.circleRule2,
),
const SizedBox(height: 8),
_RuleItem(
icon: Icons.star_rounded,
color: const Color(0xFF7C3AED),
text: l10n.circleRule3,
),
const SizedBox(height: 8),
_RuleItem(
icon: Icons.repeat_rounded,
color: const Color(0xFF10B981),
text: l10n.circleRule4,
),
const SizedBox(height: 8),
_RuleItem(
icon: Icons.account_tree_rounded,
color: const Color(0xFFF59E0B),
text: l10n.circleRule5,
),
const SizedBox(height: 8),
_RuleItem(
icon: Icons.redeem_rounded,
color: const Color(0xFF10B981),
text: l10n.circleRule6,
),
],
),
),
);
}
}
// ── Circle Member Preview ─────────────────────────────────────────────────────
class _CirclePreviewList extends ConsumerWidget {
final Color cardColor;
const _CirclePreviewList({required this.cardColor});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final async = ref.watch(myCircleProvider);
return async.when(
loading: () => const SizedBox(
height: 60, child: Center(child: CircularProgressIndicator())),
error: (_, __) => const SizedBox.shrink(),
data: (result) {
if (result.items.isEmpty) {
return _EmptyCard(cardColor: cardColor, message: l10n.noCircleMembersMessage);
}
final preview = result.items.take(3).toList();
return Card(
color: cardColor,
elevation: 0,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
clipBehavior: Clip.antiAlias,
child: Column(
children: preview.map((m) => _CircleMemberTile(member: m)).toList(),
),
);
},
);
}
}
class _CircleMemberTile extends StatelessWidget {
final CircleMember member;
const _CircleMemberTile({required this.member});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final statusColor = member.isActive
? const Color(0xFF10B981)
: member.status == 'EXPIRED'
? Colors.grey
: const Color(0xFFF59E0B);
final statusLabel = switch (member.status) {
'PENDING' => l10n.pendingPaymentStatus,
'ACTIVE' => l10n.activatedStatus,
'REWARDED' => l10n.rewardedStatus,
'EXPIRED' => l10n.expiredStatus,
_ => member.status,
};
final levelLabel = member.level == 1 ? 'L1' : 'L2';
return ListTile(
leading: CircleAvatar(
backgroundColor: const Color(0xFF7C3AED).withAlpha(20),
child: Text(
levelLabel,
style: const TextStyle(
color: Color(0xFF7C3AED),
fontWeight: FontWeight.bold,
fontSize: 12),
),
),
title: Text(
member.referredUserId.length > 8
? '${member.referredUserId.substring(0, 8)}...'
: member.referredUserId,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
subtitle: Text(
'${l10n.joinedAtLabel} ${_formatDate(member.joinedAt)}',
style: const TextStyle(fontSize: 12),
),
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statusColor.withAlpha(20),
borderRadius: BorderRadius.circular(20),
),
child: Text(
statusLabel,
style: TextStyle(
fontSize: 12,
color: statusColor,
fontWeight: FontWeight.w500),
),
),
);
}
String _formatDate(DateTime dt) =>
'${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
}
// ── Points Preview List ───────────────────────────────────────────────────────
class _PointsPreviewList extends ConsumerWidget {
final Color cardColor;
const _PointsPreviewList({required this.cardColor});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final async = ref.watch(myPointsProvider);
return async.when(
loading: () => const SizedBox(
height: 60, child: Center(child: CircularProgressIndicator())),
error: (_, __) => const SizedBox.shrink(),
data: (result) {
if (result.transactions.isEmpty) {
return _EmptyCard(cardColor: cardColor, message: l10n.noPointsHistoryMessage);
}
final preview = result.transactions.take(3).toList();
return Card(
color: cardColor,
elevation: 0,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
clipBehavior: Clip.antiAlias,
child: Column(
children:
preview.map((t) => _PointsTile(tx: t)).toList(),
),
);
},
);
}
}
class _PointsTile extends StatelessWidget {
final PointTransaction tx;
const _PointsTile({required this.tx});
@override
Widget build(BuildContext context) {
final color =
tx.isEarned ? const Color(0xFF10B981) : const Color(0xFFEF4444);
final sign = tx.isEarned ? '+' : '';
return ListTile(
leading: CircleAvatar(
backgroundColor: color.withAlpha(20),
child: Icon(
tx.isEarned ? Icons.add_circle_outline : Icons.remove_circle_outline,
color: color,
size: 20,
),
),
title: Text(tx.typeLabel,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
subtitle: Text(
_formatDate(tx.createdAt),
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
trailing: Text(
'$sign${tx.delta} pts',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: color),
),
);
}
String _formatDate(DateTime dt) =>
'${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
}
// ── Full list pages (circle & points) ────────────────────────────────────────
class _CircleListPage extends ConsumerWidget {
const _CircleListPage();
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final async = ref.watch(myCircleProvider);
return Scaffold(
appBar: AppBar(title: Text(l10n.referralTabPersonal)),
body: async.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('${l10n.loadFailed}: $e')),
data: (result) => result.items.isEmpty
? Center(child: Text(l10n.noCircleMembersMessage))
: ListView.separated(
itemCount: result.items.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (_, i) =>
_CircleMemberTile(member: result.items[i]),
),
),
);
}
}
class _PointsHistoryPage extends ConsumerWidget {
const _PointsHistoryPage();
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final async = ref.watch(myPointsProvider);
return Scaffold(
appBar: AppBar(title: Text(l10n.pointsHistorySection)),
body: async.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('${l10n.loadFailed}: $e')),
data: (result) => result.transactions.isEmpty
? Center(child: Text(l10n.noPointsHistoryMessage))
: ListView.separated(
itemCount: result.transactions.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (_, i) =>
_PointsTile(tx: result.transactions[i]),
),
),
);
}
}
// ══════════════════════════════════════════════════════════════════════════════
// Shared / Tenant tab widgets (unchanged logic, extracted)
// ══════════════════════════════════════════════════════════════════════════════
class _ReferralCodeCard extends StatelessWidget {
final ReferralInfo info;
final Color cardColor;
const _ReferralCodeCard({required this.info, required this.cardColor});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Card(
color: cardColor,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.yourReferralCodeLabel,
style: const TextStyle(color: Colors.grey, fontSize: 13),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: Text(
info.referralCode,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
letterSpacing: 2,
color: AppColors.primary,
),
),
),
IconButton(
icon: const Icon(Icons.copy, color: AppColors.primary),
tooltip: l10n.copyReferralCodeTooltip,
onPressed: () => _copy(context, info.referralCode),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
icon: const Icon(Icons.link, size: 18),
label: Text(l10n.copyInviteLinkButton),
onPressed: () => _copy(context, info.shareUrl),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primary,
side: const BorderSide(color: AppColors.primary),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
),
),
const SizedBox(width: 8),
Expanded(
child: FilledButton.icon(
icon: const Icon(Icons.share, size: 18),
label: Text(l10n.shareButton),
onPressed: () => _share(context, info),
style: FilledButton.styleFrom(
backgroundColor: AppColors.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
),
),
],
),
],
),
),
);
}
void _copy(BuildContext context, String text) {
final l10n = AppLocalizations.of(context);
Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.copiedToClipboard),
duration: const Duration(seconds: 2)),
);
}
void _share(BuildContext context, ReferralInfo info) {
final text =
'邀请你使用 IT0 智能体管理平台,注册即可获得积分奖励!\n推荐码:${info.referralCode}\n链接:${info.shareUrl}';
_copy(context, text);
}
}
class _StatsRow extends StatelessWidget {
final ReferralInfo info;
final Color cardColor;
const _StatsRow({required this.info, required this.cardColor});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Row(
children: [
_StatCard(
cardColor: cardColor,
label: l10n.referredLabel,
value: '${info.directCount}',
unit: l10n.peopleUnit,
color: const Color(0xFF6366F1),
),
const SizedBox(width: 10),
_StatCard(
cardColor: cardColor,
label: l10n.activatedLabel,
value: '${info.activeCount}',
unit: l10n.peopleUnit,
color: const Color(0xFF10B981),
),
const SizedBox(width: 10),
_StatCard(
cardColor: cardColor,
label: l10n.pendingCreditsLabel,
value: info.pendingCreditFormatted,
unit: '',
color: const Color(0xFFF59E0B),
),
],
);
}
}
class _StatCard extends StatelessWidget {
final Color cardColor;
final String label;
final String value;
final String unit;
final Color color;
const _StatCard({
required this.cardColor,
required this.label,
required this.value,
required this.unit,
required this.color,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: Card(
color: cardColor,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: const TextStyle(fontSize: 12, color: Colors.grey)),
const SizedBox(height: 4),
RichText(
text: TextSpan(
children: [
TextSpan(
text: value,
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: color,
),
),
if (unit.isNotEmpty)
TextSpan(
text: unit,
style:
TextStyle(fontSize: 13, color: color.withAlpha(180)),
),
],
),
),
],
),
),
),
);
}
}
class _RewardRulesCard extends StatelessWidget {
final Color cardColor;
const _RewardRulesCard({required this.cardColor});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Card(
color: cardColor,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.card_giftcard,
color: Color(0xFFF59E0B), size: 20),
const SizedBox(width: 6),
Text(
l10n.rewardRulesTitle,
style: const TextStyle(
fontWeight: FontWeight.w600, fontSize: 15),
),
],
),
const SizedBox(height: 12),
_RuleItem(
icon: Icons.star_rounded,
color: const Color(0xFF6366F1),
text: l10n.proReferralReward,
),
const SizedBox(height: 8),
_RuleItem(
icon: Icons.star_rounded,
color: const Color(0xFF7C3AED),
text: l10n.enterpriseReferralReward,
),
const SizedBox(height: 8),
_RuleItem(
icon: Icons.repeat_rounded,
color: const Color(0xFF10B981),
text: l10n.renewalBonusReward,
),
const SizedBox(height: 8),
_RuleItem(
icon: Icons.account_balance_wallet_outlined,
color: const Color(0xFFF59E0B),
text: l10n.creditDeductionReward,
),
],
),
),
);
}
}
class _RuleItem extends StatelessWidget {
final IconData icon;
final Color color;
final String text;
const _RuleItem({required this.icon, required this.color, required this.text});
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: color, size: 16),
const SizedBox(width: 8),
Expanded(
child: Text(text, style: const TextStyle(fontSize: 13, height: 1.4)),
),
],
);
}
}
class _SectionHeader extends StatelessWidget {
final String title;
final VoidCallback onTap;
const _SectionHeader({required this.title, required this.onTap});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title,
style: const TextStyle(
fontWeight: FontWeight.w600, fontSize: 16)),
TextButton(onPressed: onTap, child: Text(l10n.viewAllButton)),
],
);
}
}
class _EmptyCard extends StatelessWidget {
final Color cardColor;
final String message;
const _EmptyCard({required this.cardColor, required this.message});
@override
Widget build(BuildContext context) {
return Card(
color: cardColor,
elevation: 0,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Center(
child: Text(message,
style: const TextStyle(color: Colors.grey, fontSize: 13)),
),
),
);
}
}
class _ReferralPreviewList extends ConsumerWidget {
final Color cardColor;
const _ReferralPreviewList({required this.cardColor});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final async = ref.watch(referralListProvider);
return async.when(
loading: () => const SizedBox(
height: 60, child: Center(child: CircularProgressIndicator())),
error: (_, __) => const SizedBox.shrink(),
data: (result) {
if (result.items.isEmpty) {
return _EmptyCard(
cardColor: cardColor, message: l10n.noReferralsMessage);
}
final preview = result.items.take(3).toList();
return Card(
color: cardColor,
elevation: 0,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
clipBehavior: Clip.antiAlias,
child: Column(
children: preview.map((item) => _ReferralTile(item: item)).toList(),
),
);
},
);
}
}
class _ReferralTile extends StatelessWidget {
final ReferralItem item;
const _ReferralTile({required this.item});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final statusColor = item.isActive
? const Color(0xFF10B981)
: item.status == 'EXPIRED'
? Colors.grey
: const Color(0xFFF59E0B);
final statusLabel = switch (item.status) {
'PENDING' => l10n.pendingPaymentStatus,
'ACTIVE' => l10n.activeStatus,
'REWARDED' => l10n.rewardedStatus,
'EXPIRED' => l10n.expiredStatus,
_ => item.status,
};
return ListTile(
leading: CircleAvatar(
backgroundColor: statusColor.withAlpha(30),
child: Icon(
item.isActive ? Icons.check_circle : Icons.pending,
color: statusColor,
size: 20,
),
),
title: Text(
item.referredTenantId.length > 8
? '${item.referredTenantId.substring(0, 8)}...'
: item.referredTenantId,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
subtitle: Text(
'${l10n.registeredAt} ${_formatDate(item.registeredAt)}',
style: const TextStyle(fontSize: 12),
),
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statusColor.withAlpha(20),
borderRadius: BorderRadius.circular(20),
),
child: Text(
statusLabel,
style: TextStyle(
fontSize: 12,
color: statusColor,
fontWeight: FontWeight.w500),
),
),
);
}
String _formatDate(DateTime dt) =>
'${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
}
class _RewardPreviewList extends ConsumerWidget {
final Color cardColor;
const _RewardPreviewList({required this.cardColor});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final async = ref.watch(pendingRewardsProvider);
return async.when(
loading: () => const SizedBox(
height: 60, child: Center(child: CircularProgressIndicator())),
error: (_, __) => const SizedBox.shrink(),
data: (result) {
if (result.items.isEmpty) {
return _EmptyCard(
cardColor: cardColor, message: l10n.noPendingRewardsMessage);
}
final preview = result.items.take(3).toList();
return Card(
color: cardColor,
elevation: 0,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
clipBehavior: Clip.antiAlias,
child: Column(
children: preview.map((item) => _RewardTile(item: item)).toList(),
),
);
},
);
}
}
class _RewardTile extends StatelessWidget {
final RewardItem item;
const _RewardTile({required this.item});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return ListTile(
leading: CircleAvatar(
backgroundColor: const Color(0xFFF59E0B).withAlpha(30),
child: const Icon(Icons.attach_money,
color: Color(0xFFF59E0B), size: 20),
),
title: Text(
item.amountFormatted,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF10B981)),
),
subtitle: Text(item.triggerLabel,
style: const TextStyle(fontSize: 12)),
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFFF59E0B).withAlpha(20),
borderRadius: BorderRadius.circular(20),
),
child: Text(
l10n.pendingDeductionStatus,
style: const TextStyle(
fontSize: 12,
color: Color(0xFFF59E0B),
fontWeight: FontWeight.w500),
),
),
);
}
}
class _ReferralListPage extends ConsumerWidget {
const _ReferralListPage();
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final async = ref.watch(referralListProvider);
return Scaffold(
appBar: AppBar(title: Text(l10n.referralRecordsSection)),
body: async.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('${l10n.loadFailed}: $e')),
data: (result) => result.items.isEmpty
? Center(child: Text(l10n.noReferralsMessage))
: ListView.separated(
itemCount: result.items.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (_, i) => _ReferralTile(item: result.items[i]),
),
),
);
}
}
class _RewardListPage extends ConsumerWidget {
const _RewardListPage();
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final async = ref.watch(allRewardsProvider);
return Scaffold(
appBar: AppBar(title: Text(l10n.rewardHistoryPageTitle)),
body: async.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('${l10n.loadFailed}: $e')),
data: (result) => result.items.isEmpty
? Center(child: Text(l10n.noRewardsHistoryMessage))
: ListView.separated(
itemCount: result.items.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (_, i) => _RewardTile(item: result.items[i]),
),
),
);
}
}