fix
This commit is contained in:
@@ -8,10 +8,13 @@
|
|||||||
/// - Logout button
|
/// - Logout button
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:worker/core/constants/ui_constants.dart';
|
import 'package:worker/core/constants/ui_constants.dart';
|
||||||
import 'package:worker/core/database/hive_initializer.dart';
|
import 'package:worker/core/database/hive_initializer.dart';
|
||||||
@@ -33,93 +36,35 @@ class AccountPage extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final userInfoAsync = ref.watch(userInfoProvider);
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF4F6F8),
|
backgroundColor: const Color(0xFFF4F6F8),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: userInfoAsync.when(
|
child: RefreshIndicator(
|
||||||
loading: () => const Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
CircularProgressIndicator(color: AppColors.primaryBlue),
|
|
||||||
SizedBox(height: AppSpacing.md),
|
|
||||||
Text(
|
|
||||||
'Đang tải thông tin...',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: AppColors.grey500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
error: (error, stack) => Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const FaIcon(
|
|
||||||
FontAwesomeIcons.circleExclamation,
|
|
||||||
size: 64,
|
|
||||||
color: AppColors.danger,
|
|
||||||
),
|
|
||||||
const SizedBox(height: AppSpacing.lg),
|
|
||||||
const Text(
|
|
||||||
'Không thể tải thông tin tài khoản',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: AppSpacing.md),
|
|
||||||
Text(
|
|
||||||
error.toString(),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: AppColors.grey500,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: AppSpacing.lg),
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: () =>
|
|
||||||
ref.read(userInfoProvider.notifier).refresh(),
|
|
||||||
icon:
|
|
||||||
const FaIcon(FontAwesomeIcons.arrowsRotate, size: 16),
|
|
||||||
label: const Text('Thử lại'),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: AppColors.primaryBlue,
|
|
||||||
foregroundColor: AppColors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
data: (userInfo) => RefreshIndicator(
|
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
await ref.read(userInfoProvider.notifier).refresh();
|
await ref.read(userInfoProvider.notifier).refresh();
|
||||||
},
|
},
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
child: Column(
|
child: Column(
|
||||||
spacing: AppSpacing.md,
|
|
||||||
children: [
|
children: [
|
||||||
// Simple Header
|
// Simple Header
|
||||||
_buildHeader(),
|
_buildHeader(),
|
||||||
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// User Profile Card with API data
|
// User Profile Card - only this depends on provider
|
||||||
_buildProfileCard(context, userInfo),
|
const _ProfileCardSection(),
|
||||||
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// Account Menu Section
|
// Account Menu Section - independent
|
||||||
_buildAccountMenu(context),
|
_buildAccountMenu(context),
|
||||||
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// Support Section
|
// Support Section - independent
|
||||||
_buildSupportSection(context),
|
_buildSupportSection(context),
|
||||||
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// Logout Button
|
// Logout Button - independent (uses ref only for logout action)
|
||||||
_buildLogoutButton(context, ref),
|
_LogoutButton(),
|
||||||
|
|
||||||
const SizedBox(height: AppSpacing.lg),
|
const SizedBox(height: AppSpacing.lg),
|
||||||
],
|
],
|
||||||
@@ -127,7 +72,6 @@ class AccountPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,138 +101,6 @@ class AccountPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build user profile card with avatar and info
|
|
||||||
Widget _buildProfileCard(
|
|
||||||
BuildContext context,
|
|
||||||
domain.UserInfo userInfo,
|
|
||||||
) {
|
|
||||||
return Container(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
|
||||||
padding: const EdgeInsets.all(AppSpacing.md),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withValues(alpha: 0.05),
|
|
||||||
blurRadius: 8,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
// Avatar with API data or gradient fallback
|
|
||||||
userInfo.avatarUrl != null
|
|
||||||
? ClipOval(
|
|
||||||
child: CachedNetworkImage(
|
|
||||||
imageUrl: userInfo.avatarUrl!,
|
|
||||||
width: 80,
|
|
||||||
height: 80,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
placeholder: (context, url) => Container(
|
|
||||||
width: 80,
|
|
||||||
height: 80,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [Color(0xFF005B9A), Color(0xFF38B6FF)],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: const Center(
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
color: Colors.white,
|
|
||||||
strokeWidth: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
errorWidget: (context, url, error) => Container(
|
|
||||||
width: 80,
|
|
||||||
height: 80,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [Color(0xFF005B9A), Color(0xFF38B6FF)],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
userInfo.initials,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 32,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Container(
|
|
||||||
width: 80,
|
|
||||||
height: 80,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [Color(0xFF005B9A), Color(0xFF38B6FF)],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
userInfo.initials,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 32,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: AppSpacing.md),
|
|
||||||
|
|
||||||
// User info from API
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
spacing: AppSpacing.xs,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
userInfo.fullName,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColors.grey900,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'${_getRoleDisplayName(userInfo.role)} · Hạng ${userInfo.tierDisplayName}',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: AppColors.grey500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (userInfo.phoneNumber != null)
|
|
||||||
Text(
|
|
||||||
userInfo.phoneNumber!,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: AppColors.primaryBlue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build account menu section
|
/// Build account menu section
|
||||||
Widget _buildAccountMenu(BuildContext context) {
|
Widget _buildAccountMenu(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
@@ -378,14 +190,14 @@ class AccountPage extends ConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Section title
|
// Section title
|
||||||
Padding(
|
const Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
AppSpacing.md,
|
AppSpacing.md,
|
||||||
AppSpacing.md,
|
AppSpacing.md,
|
||||||
AppSpacing.md,
|
AppSpacing.md,
|
||||||
AppSpacing.sm,
|
AppSpacing.sm,
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'Hỗ trợ',
|
'Hỗ trợ',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@@ -434,29 +246,6 @@ class AccountPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build logout button
|
|
||||||
Widget _buildLogoutButton(BuildContext context, WidgetRef ref) {
|
|
||||||
return Container(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
|
||||||
width: double.infinity,
|
|
||||||
child: OutlinedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
_showLogoutConfirmation(context, ref);
|
|
||||||
},
|
|
||||||
icon: const FaIcon(FontAwesomeIcons.arrowRightFromBracket, size: 18),
|
|
||||||
label: const Text('Đăng xuất'),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
foregroundColor: AppColors.danger,
|
|
||||||
side: const BorderSide(color: AppColors.danger, width: 1.5),
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(AppRadius.button),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show coming soon message
|
/// Show coming soon message
|
||||||
void _showComingSoon(BuildContext context) {
|
void _showComingSoon(BuildContext context) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -499,6 +288,324 @@ class AccountPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Profile Card Section Widget
|
||||||
|
///
|
||||||
|
/// Isolated widget that depends on userInfoProvider.
|
||||||
|
/// Shows loading/error/data states independently.
|
||||||
|
class _ProfileCardSection extends ConsumerWidget {
|
||||||
|
const _ProfileCardSection();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final userInfoAsync = ref.watch(userInfoProvider);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
|
child: userInfoAsync.when(
|
||||||
|
loading: () => _buildLoadingCard(),
|
||||||
|
error: (error, stack) => _buildErrorCard(context, ref, error),
|
||||||
|
data: (userInfo) => _buildProfileCard(context, userInfo),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoadingCard() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(AppSpacing.md),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.05),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Avatar placeholder
|
||||||
|
Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: AppColors.grey100,
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: AppColors.primaryBlue,
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: AppSpacing.md),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 20,
|
||||||
|
width: 150,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.grey100,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
height: 14,
|
||||||
|
width: 100,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.grey100,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildErrorCard(BuildContext context, WidgetRef ref, Object error) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(AppSpacing.md),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.05),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: AppColors.grey100,
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: FaIcon(
|
||||||
|
FontAwesomeIcons.circleExclamation,
|
||||||
|
color: AppColors.danger,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: AppSpacing.md),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Không thể tải thông tin',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColors.grey900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => ref.read(userInfoProvider.notifier).refresh(),
|
||||||
|
child: const Text(
|
||||||
|
'Nhấn để thử lại',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: AppColors.primaryBlue,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildProfileCard(BuildContext context, domain.UserInfo userInfo) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(AppSpacing.md),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.05),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Avatar with API data or gradient fallback
|
||||||
|
userInfo.avatarUrl != null
|
||||||
|
? ClipOval(
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: userInfo.avatarUrl!,
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: (context, url) => Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [Color(0xFF005B9A), Color(0xFF38B6FF)],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
errorWidget: (context, url, error) => Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [Color(0xFF005B9A), Color(0xFF38B6FF)],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
userInfo.initials,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [Color(0xFF005B9A), Color(0xFF38B6FF)],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
userInfo.initials,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: AppSpacing.md),
|
||||||
|
|
||||||
|
// User info from API
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
userInfo.fullName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.grey900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: AppSpacing.xs),
|
||||||
|
Text(
|
||||||
|
'${_getRoleDisplayName(userInfo.role)} · Hạng ${userInfo.tierDisplayName}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: AppColors.grey500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (userInfo.phoneNumber != null) ...[
|
||||||
|
const SizedBox(height: AppSpacing.xs),
|
||||||
|
Text(
|
||||||
|
userInfo.phoneNumber!,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: AppColors.primaryBlue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get Vietnamese display name for user role
|
||||||
|
String _getRoleDisplayName(UserRole role) {
|
||||||
|
switch (role) {
|
||||||
|
case UserRole.customer:
|
||||||
|
return 'Khách hàng';
|
||||||
|
case UserRole.distributor:
|
||||||
|
return 'Đại lý phân phối';
|
||||||
|
case UserRole.admin:
|
||||||
|
return 'Quản trị viên';
|
||||||
|
case UserRole.staff:
|
||||||
|
return 'Nhân viên';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Logout Button Widget
|
||||||
|
///
|
||||||
|
/// Isolated widget that handles logout functionality.
|
||||||
|
class _LogoutButton extends ConsumerWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
|
width: double.infinity,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
_showLogoutConfirmation(context, ref);
|
||||||
|
},
|
||||||
|
icon: const FaIcon(FontAwesomeIcons.arrowRightFromBracket, size: 18),
|
||||||
|
label: const Text('Đăng xuất'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: AppColors.danger,
|
||||||
|
side: const BorderSide(color: AppColors.danger, width: 1.5),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.button),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Show logout confirmation dialog
|
/// Show logout confirmation dialog
|
||||||
void _showLogoutConfirmation(BuildContext context, WidgetRef ref) {
|
void _showLogoutConfirmation(BuildContext context, WidgetRef ref) {
|
||||||
@@ -527,16 +634,17 @@ class AccountPage extends ConsumerWidget {
|
|||||||
/// Handles the complete logout process:
|
/// Handles the complete logout process:
|
||||||
/// 1. Close confirmation dialog
|
/// 1. Close confirmation dialog
|
||||||
/// 2. Show loading indicator
|
/// 2. Show loading indicator
|
||||||
/// 3. Clear Hive local data
|
/// 3. Clear ALL Hive local data (reset, not just user data)
|
||||||
/// 4. Call auth provider logout (clears session, gets new public session)
|
/// 4. Clear ALL Flutter Secure Storage keys
|
||||||
/// 5. Navigate to login screen (handled by router redirect)
|
/// 5. Call auth provider logout (clears session, gets new public session)
|
||||||
/// 6. Show success message
|
/// 6. Navigate to login screen (handled by router redirect)
|
||||||
|
/// 7. Show success message
|
||||||
Future<void> _performLogout(BuildContext context, WidgetRef ref) async {
|
Future<void> _performLogout(BuildContext context, WidgetRef ref) async {
|
||||||
// Close confirmation dialog
|
// Close confirmation dialog
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
// Show loading dialog
|
// Show loading dialog
|
||||||
showDialog<void>(
|
unawaited(showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (context) => const Center(
|
builder: (context) => const Center(
|
||||||
@@ -554,15 +662,21 @@ class AccountPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Clear Hive local data (cart, favorites, cached data)
|
// 1. Clear ALL Hive data (complete reset)
|
||||||
await HiveInitializer.logout();
|
await HiveInitializer.reset();
|
||||||
|
|
||||||
// Call auth provider logout
|
// 2. Clear ALL Flutter Secure Storage keys
|
||||||
|
const secureStorage = FlutterSecureStorage(
|
||||||
|
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||||
|
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
|
||||||
|
);
|
||||||
|
await secureStorage.deleteAll();
|
||||||
|
|
||||||
|
// 3. Call auth provider logout
|
||||||
// This will:
|
// This will:
|
||||||
// - Clear FlutterSecureStorage session
|
|
||||||
// - Clear FrappeAuthService session
|
// - Clear FrappeAuthService session
|
||||||
// - Get new public session for login/registration
|
// - Get new public session for login/registration
|
||||||
// - Update auth state to null (logged out)
|
// - Update auth state to null (logged out)
|
||||||
@@ -604,18 +718,4 @@ class AccountPage extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get Vietnamese display name for user role
|
|
||||||
String _getRoleDisplayName(UserRole role) {
|
|
||||||
switch (role) {
|
|
||||||
case UserRole.customer:
|
|
||||||
return 'Khách hàng';
|
|
||||||
case UserRole.distributor:
|
|
||||||
return 'Đại lý phân phối';
|
|
||||||
case UserRole.admin:
|
|
||||||
return 'Quản trị viên';
|
|
||||||
case UserRole.staff:
|
|
||||||
return 'Nhân viên';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.0.1+18
|
version: 1.0.1+20
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.10.0
|
sdk: ^3.10.0
|
||||||
|
|||||||
Reference in New Issue
Block a user