574 lines
23 KiB
Dart
574 lines
23 KiB
Dart
import 'dart:io';
|
|
import 'dart:ui';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:share_plus/share_plus.dart';
|
|
import 'package:cross_file/cross_file.dart';
|
|
import 'auth_screen.dart';
|
|
import '../../../core/network/dio_client.dart';
|
|
import '../providers/auth_provider.dart';
|
|
import '../../transactions/providers/providers.dart';
|
|
import '../../transactions/providers/paginated_transaction_provider.dart';
|
|
import '../../../core/theme/theme_provider.dart';
|
|
import '../../business/providers/business_mode_provider.dart';
|
|
import '../../business/providers/business_provider.dart';
|
|
import '../../projects/providers/project_mode_provider.dart';
|
|
import '../../projects/providers/project_provider.dart';
|
|
import '../../inventory/providers/products_provider.dart';
|
|
import '../../inventory/providers/product_categories_provider.dart';
|
|
import '../../inventory/providers/inventory_items_provider.dart';
|
|
import '../../inventory/providers/inventory_valuation_provider.dart';
|
|
import '../../inventory/providers/commodity_rates_provider.dart';
|
|
import '../../vendor/providers/vendors_provider.dart';
|
|
import '../../vendor/providers/purchase_orders_provider.dart';
|
|
import '../../sales/providers/customers_provider.dart';
|
|
import '../../sales/providers/invoices_provider.dart';
|
|
import '../../business/presentation/settings/business_settings_screen.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
class ProfileScreen extends ConsumerStatefulWidget {
|
|
const ProfileScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<ProfileScreen> createState() => _ProfileScreenState();
|
|
}
|
|
|
|
class _ProfileScreenState extends ConsumerState<ProfileScreen>
|
|
with SingleTickerProviderStateMixin {
|
|
bool _isExporting = false;
|
|
String? _profileType;
|
|
String _userName = 'Loading...';
|
|
String _userEmail = 'Loading...';
|
|
late AnimationController _animController;
|
|
late Animation<double> _fadeAnim;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_animController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 800),
|
|
);
|
|
_fadeAnim = CurvedAnimation(
|
|
parent: _animController,
|
|
curve: Curves.easeOutCubic,
|
|
);
|
|
_animController.forward();
|
|
_fetchProfileType();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_animController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _fetchProfileType() async {
|
|
try {
|
|
final res = await DioClient().dio.get('/account/setup/status');
|
|
if (mounted) {
|
|
setState(() {
|
|
_profileType = res.data['profileType'];
|
|
_userName = res.data['name'] ?? 'Kifi User';
|
|
if (_userName.isEmpty) _userName = 'Kifi User';
|
|
_userEmail = res.data['email'] ?? '';
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_userName = 'Kifi User';
|
|
_userEmail = '';
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
String _getInitials(String name) {
|
|
if (name.isEmpty || name == 'Kifi User' || name == 'Loading...')
|
|
return 'KU';
|
|
final parts = name.trim().split(' ');
|
|
if (parts.length > 1 && parts[1].isNotEmpty) {
|
|
return '${parts[0][0]}${parts[1][0]}'.toUpperCase();
|
|
}
|
|
return name.substring(0, name.length >= 2 ? 2 : 1).toUpperCase();
|
|
}
|
|
|
|
Future<void> _logout() async {
|
|
await DioClient().clearToken();
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.remove('is_business_mode');
|
|
} catch (_) {}
|
|
|
|
try {
|
|
ref.invalidate(categoryProvider);
|
|
ref.invalidate(transactionProvider);
|
|
ref.invalidate(paginatedTransactionProvider);
|
|
ref.invalidate(budgetProvider);
|
|
ref.invalidate(recurringTransactionProvider);
|
|
ref.invalidate(walletProvider);
|
|
ref.invalidate(invitationProvider);
|
|
ref.invalidate(businessProfileProvider);
|
|
ref.invalidate(businessFeatureProvider);
|
|
ref.invalidate(businessModeProvider);
|
|
ref.invalidate(productsProvider);
|
|
ref.invalidate(productCategoriesProvider);
|
|
ref.invalidate(inventoryItemsProvider);
|
|
ref.invalidate(inventoryValuationProvider);
|
|
ref.invalidate(commodityRatesProvider);
|
|
ref.invalidate(vendorsProvider);
|
|
ref.invalidate(purchaseOrdersProvider);
|
|
ref.invalidate(customersProvider);
|
|
ref.invalidate(invoicesProvider);
|
|
ref.invalidate(projectsProvider);
|
|
ref.invalidate(authControllerProvider);
|
|
} catch (_) {}
|
|
|
|
if (mounted) {
|
|
Navigator.pushAndRemoveUntil(
|
|
context,
|
|
MaterialPageRoute(builder: (_) => const AuthScreen()),
|
|
(route) => false,
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _exportData() async {
|
|
setState(() => _isExporting = true);
|
|
try {
|
|
final bytes = await ref.read(apiRepositoryProvider).exportTransactions();
|
|
final tempDir = await getTemporaryDirectory();
|
|
final file = File('${tempDir.path}/transactions_export.csv');
|
|
await file.writeAsBytes(bytes);
|
|
final xFile = XFile(file.path);
|
|
await Share.shareXFiles([
|
|
xFile,
|
|
], text: 'Here is my Kifi transactions export.');
|
|
} catch (e) {
|
|
if (mounted)
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(SnackBar(content: Text('Export failed: $e')));
|
|
} finally {
|
|
if (mounted) setState(() => _isExporting = false);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
final primaryColor = Theme.of(context).primaryColor;
|
|
final themeMode = ref.watch(themeProvider);
|
|
|
|
return Scaffold(
|
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
|
extendBodyBehindAppBar: true,
|
|
appBar: AppBar(
|
|
title: const Text(
|
|
'Profile',
|
|
style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1.2),
|
|
),
|
|
backgroundColor: Colors.transparent,
|
|
elevation: 0,
|
|
centerTitle: true,
|
|
flexibleSpace: ClipRect(
|
|
child: BackdropFilter(
|
|
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
|
child: Container(
|
|
color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.5),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
body: Stack(
|
|
children: [
|
|
// Background Decorative Elements
|
|
Positioned(
|
|
top: -50,
|
|
right: -50,
|
|
child: Container(
|
|
width: 200,
|
|
height: 200,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: primaryColor.withOpacity(isDark ? 0.2 : 0.1),
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
bottom: -100,
|
|
left: -50,
|
|
child: Container(
|
|
width: 300,
|
|
height: 300,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: Colors.purple.withOpacity(isDark ? 0.15 : 0.08),
|
|
),
|
|
),
|
|
),
|
|
// Main Content
|
|
SafeArea(
|
|
child: FadeTransition(
|
|
opacity: _fadeAnim,
|
|
child: Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 600),
|
|
child: ListView(
|
|
physics: const BouncingScrollPhysics(),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 24.0,
|
|
vertical: 24.0,
|
|
),
|
|
children: [
|
|
// Avatar Section
|
|
Center(
|
|
child: Hero(
|
|
tag: 'profile_avatar',
|
|
child: Container(
|
|
width: 120,
|
|
height: 120,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
primaryColor.withOpacity(0.8),
|
|
primaryColor.withOpacity(0.5),
|
|
],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: primaryColor.withOpacity(0.3),
|
|
blurRadius: 20,
|
|
offset: const Offset(0, 10),
|
|
),
|
|
],
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
_getInitials(_userName),
|
|
style: const TextStyle(
|
|
fontSize: 40,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
letterSpacing: 2,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
Text(
|
|
_userName,
|
|
style: Theme.of(context).textTheme.headlineSmall
|
|
?.copyWith(fontWeight: FontWeight.bold),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
if (_userEmail.isNotEmpty &&
|
|
_userEmail != 'Loading...') ...[
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
_userEmail,
|
|
style: TextStyle(
|
|
color: isDark
|
|
? Colors.grey.shade400
|
|
: Colors.grey.shade600,
|
|
fontSize: 16,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
const SizedBox(height: 40),
|
|
|
|
// Glassmorphism Card for Settings
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(24),
|
|
child: BackdropFilter(
|
|
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(
|
|
context,
|
|
).cardColor.withOpacity(isDark ? 0.3 : 0.6),
|
|
borderRadius: BorderRadius.circular(24),
|
|
border: Border.all(
|
|
color: Colors.white.withOpacity(
|
|
isDark ? 0.05 : 0.2,
|
|
),
|
|
width: 1.5,
|
|
),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
_buildSettingsTile(
|
|
context: context,
|
|
icon: LucideIcons.downloadCloud,
|
|
iconColor: Colors.blue,
|
|
title: 'Export Data to CSV',
|
|
trailing: _isExporting
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
),
|
|
)
|
|
: const Icon(
|
|
LucideIcons.chevronRight,
|
|
size: 20,
|
|
color: Colors.grey,
|
|
),
|
|
onTap: _isExporting ? null : _exportData,
|
|
),
|
|
_buildDivider(context, isDark),
|
|
_buildSettingsTile(
|
|
context: context,
|
|
icon: LucideIcons.palette,
|
|
iconColor: Colors.purple,
|
|
title: 'Appearance',
|
|
trailing: SegmentedButton<ThemeMode>(
|
|
segments: const [
|
|
ButtonSegment(
|
|
value: ThemeMode.light,
|
|
icon: Icon(LucideIcons.sun, size: 16),
|
|
),
|
|
ButtonSegment(
|
|
value: ThemeMode.system,
|
|
icon: Icon(
|
|
LucideIcons.monitor,
|
|
size: 16,
|
|
),
|
|
),
|
|
ButtonSegment(
|
|
value: ThemeMode.dark,
|
|
icon: Icon(LucideIcons.moon, size: 16),
|
|
),
|
|
],
|
|
selected: {themeMode},
|
|
onSelectionChanged:
|
|
(Set<ThemeMode> newSelection) {
|
|
ref
|
|
.read(themeProvider.notifier)
|
|
.setTheme(newSelection.first);
|
|
},
|
|
showSelectedIcon: false,
|
|
style: ButtonStyle(
|
|
visualDensity: VisualDensity.compact,
|
|
tapTargetSize:
|
|
MaterialTapTargetSize.shrinkWrap,
|
|
backgroundColor:
|
|
WidgetStateProperty.resolveWith<
|
|
Color?
|
|
>((states) {
|
|
if (states.contains(
|
|
WidgetState.selected,
|
|
)) {
|
|
return primaryColor.withOpacity(
|
|
0.2,
|
|
);
|
|
}
|
|
return null;
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
_buildDivider(context, isDark),
|
|
Consumer(
|
|
builder: (context, ref, child) {
|
|
final isBusinessMode = ref.watch(
|
|
businessModeProvider,
|
|
);
|
|
return Column(
|
|
children: [
|
|
if (_profileType == 'BUSINESS') ...[
|
|
_buildSwitchTile(
|
|
context: context,
|
|
icon: LucideIcons.briefcase,
|
|
iconColor: Colors.orange,
|
|
title: 'Business Mode',
|
|
subtitle: 'Inventory and Sales Hub',
|
|
value: isBusinessMode,
|
|
onChanged: (val) => ref
|
|
.read(
|
|
businessModeProvider.notifier,
|
|
)
|
|
.toggleMode(),
|
|
),
|
|
if (isBusinessMode) ...[
|
|
_buildDivider(context, isDark),
|
|
_buildSettingsTile(
|
|
context: context,
|
|
icon: LucideIcons.settings2,
|
|
iconColor: Colors.grey.shade600,
|
|
title: 'Business Settings',
|
|
subtitle:
|
|
'Configure Taxes, Barcodes, etc.',
|
|
trailing: const Icon(
|
|
LucideIcons.chevronRight,
|
|
size: 20,
|
|
color: Colors.grey,
|
|
),
|
|
onTap: () => Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) =>
|
|
const BusinessSettingsScreen(),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
_buildDivider(context, isDark),
|
|
],
|
|
],
|
|
);
|
|
},
|
|
),
|
|
_buildDivider(context, isDark),
|
|
_buildSettingsTile(
|
|
context: context,
|
|
icon: LucideIcons.helpCircle,
|
|
iconColor: Colors.green,
|
|
title: 'Help & Support',
|
|
trailing: const Icon(
|
|
LucideIcons.chevronRight,
|
|
size: 20,
|
|
color: Colors.grey,
|
|
),
|
|
onTap: () {},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 48),
|
|
|
|
// Logout Button
|
|
Container(
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(16),
|
|
gradient: LinearGradient(
|
|
colors: [Colors.red.shade400, Colors.red.shade600],
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.red.withOpacity(0.3),
|
|
blurRadius: 15,
|
|
offset: const Offset(0, 5),
|
|
),
|
|
],
|
|
),
|
|
child: ElevatedButton.icon(
|
|
onPressed: () => _logout(),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.transparent,
|
|
foregroundColor: Colors.white,
|
|
shadowColor: Colors.transparent,
|
|
padding: const EdgeInsets.symmetric(vertical: 20),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
),
|
|
icon: const Icon(LucideIcons.logOut, size: 22),
|
|
label: const Text(
|
|
'Log Out',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
letterSpacing: 1.0,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 40),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSettingsTile({
|
|
required BuildContext context,
|
|
required IconData icon,
|
|
required Color iconColor,
|
|
required String title,
|
|
String? subtitle,
|
|
required Widget trailing,
|
|
VoidCallback? onTap,
|
|
}) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
return ListTile(
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
|
leading: Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: iconColor.withOpacity(isDark ? 0.2 : 0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(icon, color: iconColor, size: 24),
|
|
),
|
|
title: Text(
|
|
title,
|
|
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
|
|
),
|
|
subtitle: subtitle != null
|
|
? Text(
|
|
subtitle,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: isDark ? Colors.grey.shade400 : Colors.grey.shade600,
|
|
),
|
|
)
|
|
: null,
|
|
trailing: trailing,
|
|
onTap: onTap,
|
|
);
|
|
}
|
|
|
|
Widget _buildSwitchTile({
|
|
required BuildContext context,
|
|
required IconData icon,
|
|
required Color iconColor,
|
|
required String title,
|
|
required String subtitle,
|
|
required bool value,
|
|
required ValueChanged<bool> onChanged,
|
|
}) {
|
|
return _buildSettingsTile(
|
|
context: context,
|
|
icon: icon,
|
|
iconColor: iconColor,
|
|
title: title,
|
|
subtitle: subtitle,
|
|
trailing: Switch(
|
|
value: value,
|
|
onChanged: onChanged,
|
|
activeColor: iconColor,
|
|
),
|
|
onTap: () => onChanged(!value),
|
|
);
|
|
}
|
|
|
|
Widget _buildDivider(BuildContext context, bool isDark) {
|
|
return Divider(
|
|
height: 1,
|
|
indent: 76,
|
|
endIndent: 20,
|
|
color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.05),
|
|
);
|
|
}
|
|
}
|