Revamp stage one - individual account and account setup fixed

This commit is contained in:
2026-08-25 19:45:24 +05:30
parent 82c1a891c0
commit 53c1f62373
52 changed files with 3980 additions and 4020 deletions

View File

@@ -15,8 +15,8 @@ class DioClient {
DioClient._internal()
: dio = Dio(BaseOptions(
baseUrl: 'https://app.technobeesolutions.in/api/kifi-v2',
//baseUrl: 'http://192.168.0.104:8080/api/kifi-v2', // Local Mac IP for physical device testing
//baseUrl: 'https://app.technobeesolutions.in/api/kifi-v2',
baseUrl: 'http://192.168.0.104:8080/api/kifi-v2', // Local Mac IP for physical device testing
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
)),

View File

@@ -35,4 +35,8 @@ class CryptoService {
}
bool get isInitialized => _publicKey != null;
void clearKey() {
_publicKey = null;
}
}

View File

@@ -0,0 +1,21 @@
class MaskingUtils {
static String maskMobile(String mobile) {
if (mobile.length < 4) return mobile;
return 'XXXXXX${mobile.substring(mobile.length - 4)}';
}
static String maskPan(String pan) {
if (pan.length < 4) return pan;
return '${pan.substring(0, 2)}XXXXXX${pan.substring(pan.length - 2)}';
}
static String maskGst(String gst) {
if (gst.length < 4) return gst;
return '${gst.substring(0, 2)}XXXXXXXXXX${gst.substring(gst.length - 3)}';
}
static String maskMsme(String msme) {
if (msme.length < 4) return msme;
return '${msme.substring(0, 4)}XXXX${msme.substring(msme.length - 4)}';
}
}

View File

@@ -28,19 +28,33 @@ class AuthRepository {
Future<Map<String, dynamic>> login(String email, String password) async {
try {
await _ensureCryptoReady();
final response = await dio.post('/auth/login', data: {
'email': _crypto.encrypt(email),
'password': _crypto.encrypt(password),
});
return response.data;
return await _attemptLogin(email, password);
} on DioException catch (e) {
if (e.response?.statusCode == 500 || e.response?.statusCode == 400 || e.response?.statusCode == 401) {
_crypto.clearKey();
try {
return await _attemptLogin(email, password);
} on DioException catch (e2) {
final data = e2.response?.data;
final errorMsg = data is Map ? data['error'] : data;
throw Exception(errorMsg ?? 'Invalid email or password');
}
}
final data = e.response?.data;
final errorMsg = data is Map ? data['error'] : data;
throw Exception(errorMsg ?? 'Invalid email or password');
}
}
Future<Map<String, dynamic>> _attemptLogin(String email, String password) async {
await _ensureCryptoReady();
final response = await dio.post('/auth/login', data: {
'email': _crypto.encrypt(email),
'password': _crypto.encrypt(password),
});
return response.data;
}
Future<Map<String, dynamic>> verifyOtp(String email, String otp) async {
try {
final response = await dio.post('/auth/verify-otp', data: {

View File

@@ -6,6 +6,8 @@ import '../providers/auth_provider.dart';
import 'otp_screen.dart';
import 'forgot_password_screen.dart';
import '../../dashboard/presentation/dashboard_screen.dart';
import '../../../../core/network/dio_client.dart';
import 'setup_wizard_screen.dart';
class AuthScreen extends ConsumerStatefulWidget {
const AuthScreen({super.key});
@@ -14,14 +16,15 @@ class AuthScreen extends ConsumerStatefulWidget {
ConsumerState<AuthScreen> createState() => _AuthScreenState();
}
class _AuthScreenState extends ConsumerState<AuthScreen> {
class _AuthScreenState extends ConsumerState<AuthScreen> with SingleTickerProviderStateMixin {
bool isLogin = true;
final emailController = TextEditingController();
final passwordController = TextEditingController();
final emailController = TextEditingController(text: 'maddy23285@gmail.com');
final passwordController = TextEditingController(text: 'Algorithm@123');
void toggleMode() {
void toggleMode(bool login) {
if (isLogin == login) return;
setState(() {
isLogin = !isLogin;
isLogin = login;
});
}
@@ -34,8 +37,18 @@ class _AuthScreenState extends ConsumerState<AuthScreen> {
if (isLogin) {
final success = await ref.read(authControllerProvider.notifier).login(email, password);
if (success && mounted) {
Navigator.pushReplacement(
context, MaterialPageRoute(builder: (_) => const DashboardScreen()));
try {
final dio = DioClient().dio;
final res = await dio.get('/account/setup/status');
final status = res.data['status'];
if (status == 'COMPLETED' && mounted) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const DashboardScreen()));
} else if (mounted) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const SetupWizardScreen()));
}
} catch(e) {
if (mounted) Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const SetupWizardScreen()));
}
}
} else {
final success = await ref.read(authControllerProvider.notifier).signup(email, password);
@@ -45,12 +58,34 @@ class _AuthScreenState extends ConsumerState<AuthScreen> {
}
}
InputDecoration _buildInputDecoration(String label, IconData icon) {
return InputDecoration(
labelText: label,
prefixIcon: Icon(icon, color: Colors.grey.shade500),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18),
);
}
@override
Widget build(BuildContext context) {
ref.listen<AsyncValue<void>>(authControllerProvider, (previous, next) {
if (next.hasError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${next.error}')),
SnackBar(content: Text('${next.error}')),
);
}
});
@@ -59,92 +94,149 @@ class _AuthScreenState extends ConsumerState<AuthScreen> {
final isLoading = state.isLoading;
return Scaffold(
backgroundColor: Colors.grey.shade50,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
isLogin ? 'Welcome Back!' : 'Create Account',
style: Theme.of(context).textTheme.displayLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
isLogin ? 'Login to continue to Kifi' : 'Sign up to manage your expenses',
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
TextField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
style: const TextStyle(fontWeight: FontWeight.w500),
decoration: InputDecoration(
hintText: 'Email Address',
prefixIcon: Icon(LucideIcons.mail, color: Colors.grey),
filled: true,
fillColor: Colors.grey.shade100,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)),
),
),
const SizedBox(height: 16),
TextField(
controller: passwordController,
obscureText: true,
style: const TextStyle(fontWeight: FontWeight.w500),
decoration: InputDecoration(
hintText: 'Password',
prefixIcon: Icon(LucideIcons.lock, color: Colors.grey),
filled: true,
fillColor: Colors.grey.shade100,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)),
),
),
if (isLogin) ...[
const SizedBox(height: 8),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => const ForgotPasswordScreen()));
},
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF6C63FF),
padding: EdgeInsets.zero,
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
width: 80, height: 80,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Text('Forgot Password?', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13)),
child: Center(child: Image.asset('assets/logo.png', width: 50, height: 50, fit: BoxFit.contain)),
),
),
],
const SizedBox(height: 24),
ElevatedButton(
onPressed: isLoading ? null : submit,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6C63FF),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 0,
),
child: isLoading
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
: Text(isLogin ? 'Login' : 'Sign Up', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 32),
Text(
'Welcome to Kifi',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Manage your inventory and expenses securely.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey.shade600),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Expanded(
child: InkWell(
onTap: () => toggleMode(true),
borderRadius: BorderRadius.circular(12),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: isLogin ? Colors.white : Colors.transparent,
borderRadius: BorderRadius.circular(12),
boxShadow: isLogin ? [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 4, offset: const Offset(0, 2))] : [],
),
child: Center(child: Text('Login', style: TextStyle(fontWeight: FontWeight.w600, color: isLogin ? Colors.black87 : Colors.grey.shade600))),
),
),
),
Expanded(
child: InkWell(
onTap: () => toggleMode(false),
borderRadius: BorderRadius.circular(12),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: !isLogin ? Colors.white : Colors.transparent,
borderRadius: BorderRadius.circular(12),
boxShadow: !isLogin ? [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 4, offset: const Offset(0, 2))] : [],
),
child: Center(child: Text('Sign Up', style: TextStyle(fontWeight: FontWeight.w600, color: !isLogin ? Colors.black87 : Colors.grey.shade600))),
),
),
),
],
),
),
const SizedBox(height: 32),
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
alignment: Alignment.topCenter,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
style: const TextStyle(fontWeight: FontWeight.w500),
textInputAction: TextInputAction.next,
decoration: _buildInputDecoration(isLogin ? 'Email or Username' : 'Email Address', LucideIcons.mail),
),
const SizedBox(height: 16),
TextField(
controller: passwordController,
obscureText: true,
style: const TextStyle(fontWeight: FontWeight.w500),
textInputAction: TextInputAction.done,
onSubmitted: (_) => submit(),
decoration: _buildInputDecoration('Password', LucideIcons.lock),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: isLogin
? Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.only(top: 12),
child: TextButton(
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ForgotPasswordScreen())),
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).primaryColor,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: const Text('Forgot Password?', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13)),
),
),
)
: const SizedBox(height: 12),
),
],
),
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: isLoading ? null : submit,
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 20),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 2,
),
child: isLoading
? const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
: Text(isLogin ? 'Log In' : 'Create Account', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 0.5)),
),
),
],
),
const SizedBox(height: 16),
TextButton(
onPressed: toggleMode,
child: Text(isLogin ? 'Don\'t have an account? Sign Up' : 'Already have an account? Login'),
),
],
),
),
),
),

View File

@@ -49,112 +49,129 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
}
}
InputDecoration _buildInputDecoration(String label, IconData icon) {
return InputDecoration(
labelText: label,
prefixIcon: Icon(icon, color: Colors.grey.shade500),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18),
);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
backgroundColor: Colors.grey.shade50,
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.transparent,
leading: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(LucideIcons.chevronLeft, size: 28),
color: Colors.black87,
),
),
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.arrow_back_ios_new, size: 20),
style: IconButton.styleFrom(
backgroundColor: Colors.grey.shade100,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
const SizedBox(height: 40),
Center(
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6C63FF), Color(0xFF48C6EF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
width: 80, height: 80,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
borderRadius: BorderRadius.circular(24),
child: Center(child: Icon(LucideIcons.keyRound, size: 40, color: Theme.of(context).primaryColor)),
),
child: const Icon(LucideIcons.keyRound, color: Colors.white, size: 36),
),
),
const SizedBox(height: 32),
const Center(
child: Text(
'Forgot Password?',
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 12),
Center(
child: Text(
'Enter your email address and we\'ll send you\na verification code to reset your password.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 15, color: Colors.grey.shade600, height: 1.5),
),
),
const SizedBox(height: 48),
TextField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.done,
onSubmitted: (_) => _sendOtp(),
style: const TextStyle(fontWeight: FontWeight.w500),
decoration: InputDecoration(
hintText: 'Email Address',
prefixIcon: Icon(LucideIcons.mail, color: Colors.grey.shade500),
filled: true,
fillColor: Colors.grey.shade100,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)),
),
),
if (_errorMessage != null) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.red.shade200),
const SizedBox(height: 32),
Text(
'Forgot Password?',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center,
),
child: Row(
children: [
Icon(LucideIcons.alertCircle, color: Colors.red.shade400, size: 18),
const SizedBox(width: 10),
Expanded(child: Text(_errorMessage!, style: TextStyle(color: Colors.red.shade700, fontSize: 13))),
],
const SizedBox(height: 8),
Text(
'Enter your email address and we\'ll send you\na verification code to reset your password.',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey.shade600, height: 1.5),
),
),
],
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: _isLoading ? null : _sendOtp,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6C63FF),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 0,
const SizedBox(height: 40),
TextField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.done,
onSubmitted: (_) => _sendOtp(),
style: const TextStyle(fontWeight: FontWeight.w500),
decoration: _buildInputDecoration('Email Address', LucideIcons.mail),
),
child: _isLoading
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
: const Text('Send Reset Code', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
alignment: Alignment.topCenter,
child: _errorMessage != null
? Padding(
padding: const EdgeInsets.only(top: 16),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.red.shade200),
),
child: Row(
children: [
Icon(LucideIcons.alertCircle, color: Colors.red.shade400, size: 20),
const SizedBox(width: 12),
Expanded(child: Text(_errorMessage!, style: TextStyle(color: Colors.red.shade700, fontSize: 14, fontWeight: FontWeight.w500))),
],
),
),
)
: const SizedBox.shrink(),
),
const SizedBox(height: 40),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _sendOtp,
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 20),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 2,
),
child: _isLoading
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
: const Text('Send Reset Code', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 0.5)),
),
),
],
),
],
),
),
),
),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../providers/auth_provider.dart';
import '../../dashboard/presentation/dashboard_screen.dart';
@@ -30,7 +31,7 @@ class _OtpScreenState extends ConsumerState<OtpScreen> {
ref.listen<AsyncValue<void>>(authControllerProvider, (previous, next) {
if (next.hasError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${next.error}')),
SnackBar(content: Text('${next.error}')),
);
}
});
@@ -38,58 +39,91 @@ class _OtpScreenState extends ConsumerState<OtpScreen> {
final state = ref.watch(authControllerProvider);
final isLoading = state.isLoading;
return Scaffold(
appBar: AppBar(backgroundColor: Colors.transparent, elevation: 0),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Verify Email',
style: Theme.of(context).textTheme.displayLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Enter the 6-digit OTP sent to ${widget.email}',
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
TextField(
controller: otpController,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
maxLength: 6,
style: const TextStyle(fontSize: 24, letterSpacing: 8, fontWeight: FontWeight.bold, color: Color(0xFF6C63FF)),
decoration: InputDecoration(
hintText: '000000',
hintStyle: TextStyle(letterSpacing: 8, color: Colors.grey.shade400),
filled: true,
fillColor: Colors.grey.shade100,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)),
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
backgroundColor: Colors.grey.shade50,
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.transparent,
leading: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(LucideIcons.chevronLeft, size: 28),
color: Colors.black87,
),
),
),
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
width: 80, height: 80,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Center(child: Icon(LucideIcons.mailCheck, size: 40, color: Theme.of(context).primaryColor)),
),
const SizedBox(height: 32),
Text(
'Verify Email',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Enter the 6-digit OTP sent to\n${widget.email}',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey.shade600, height: 1.5),
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
TextField(
controller: otpController,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
maxLength: 6,
style: const TextStyle(fontSize: 24, letterSpacing: 16, fontWeight: FontWeight.bold),
decoration: InputDecoration(
hintText: '000000',
hintStyle: TextStyle(letterSpacing: 16, color: Colors.grey.shade300),
counterText: '',
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300)),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300)),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2)),
),
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: isLoading ? null : verify,
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 20),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 2,
),
child: isLoading
? const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
: const Text('Verify Account', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 0.5)),
),
),
],
),
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: isLoading ? null : verify,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6C63FF),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 0,
),
child: isLoading
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
: const Text('Verify', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
],
),
),
),
),

View File

@@ -25,6 +25,40 @@ class ProfileScreen extends ConsumerStatefulWidget {
class _ProfileScreenState extends ConsumerState<ProfileScreen> {
bool _isExporting = false;
String? _profileType;
String _userName = 'Loading...';
String _userEmail = 'Loading...';
@override
void initState() {
super.initState();
_fetchProfileType();
}
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) {
// ignore
}
}
String _getInitials(String name) {
if (name.isEmpty || name == 'Kifi User' || name == 'Loading...') return 'U';
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(BuildContext context) async {
const storage = FlutterSecureStorage();
@@ -63,131 +97,211 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: AppBar(
title: const Text('Profile'),
title: Text('Profile', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 20),
CircleAvatar(
radius: 50,
backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.1),
child: Icon(LucideIcons.user, size: 50, color: Theme.of(context).colorScheme.primary),
),
const SizedBox(height: 24),
const Text('Kifi User', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
const Text('maddy23285@gmail.com', style: TextStyle(color: Colors.grey, fontSize: 16)),
const SizedBox(height: 48),
Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Column(
children: [
ListTile(
leading: const Icon(LucideIcons.downloadCloud),
title: const Text('Export Data to CSV'),
trailing: _isExporting
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
: const Icon(LucideIcons.chevronRight),
onTap: _isExporting ? null : _exportData,
),
const Divider(height: 1),
ListTile(
leading: const Icon(LucideIcons.moon),
title: const Text('Theme'),
trailing: SegmentedButton<ThemeMode>(
segments: const [
ButtonSegment(value: ThemeMode.light, icon: Icon(LucideIcons.sun)),
ButtonSegment(value: ThemeMode.system, icon: Icon(LucideIcons.monitor)),
ButtonSegment(value: ThemeMode.dark, icon: Icon(LucideIcons.moon)),
],
selected: {ref.watch(themeProvider)},
onSelectionChanged: (Set<ThemeMode> newSelection) {
ref.read(themeProvider.notifier).setTheme(newSelection.first);
},
showSelectedIcon: false,
style: ButtonStyle(visualDensity: VisualDensity.compact),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 20),
Center(
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Center(
child: Text(
_getInitials(_userName),
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
),
),
const Divider(height: 1),
Consumer(
builder: (context, ref, child) {
final isBusinessMode = ref.watch(businessModeProvider);
return Column(
children: [
SwitchListTile(
secondary: const Icon(LucideIcons.briefcase),
title: const Text('Business Mode'),
subtitle: const Text('Inventory and sales management'),
value: isBusinessMode,
onChanged: (val) {
ref.read(businessModeProvider.notifier).toggleMode();
},
),
if (isBusinessMode) ...[
const Divider(height: 1),
ListTile(
leading: const Icon(LucideIcons.settings),
title: const Text('Business Settings'),
subtitle: const Text('Tax, Modules, POS'),
trailing: const Icon(LucideIcons.chevronRight),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => const BusinessSettingsScreen()));
},
),
],
const Divider(height: 1),
Consumer(
builder: (context, ref, child) {
final isProjectMode = ref.watch(projectModeProvider);
return SwitchListTile(
secondary: const Icon(LucideIcons.trello),
title: const Text('Project Management (Agency OS)'),
subtitle: const Text('Task boards and billable hours'),
value: isProjectMode,
onChanged: (val) {
ref.read(projectModeProvider.notifier).toggleMode();
},
);
},
)
],
);
}
),
const Divider(height: 1),
ListTile(
leading: const Icon(LucideIcons.helpCircle),
title: const Text('Help & Support'),
trailing: const Icon(LucideIcons.chevronRight),
onTap: () {},
),
const SizedBox(height: 24),
Text(
_userName,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Theme.of(context).textTheme.bodyLarge?.color),
textAlign: TextAlign.center,
),
if (_userEmail.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
_userEmail,
style: TextStyle(color: isDark ? Colors.grey.shade400 : Colors.grey.shade600, fontSize: 16),
textAlign: TextAlign.center,
),
],
),
),
const Spacer(),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => _logout(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.withOpacity(0.1),
foregroundColor: Colors.red,
padding: const EdgeInsets.symmetric(vertical: 16),
const SizedBox(height: 48),
Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(isDark ? 0.2 : 0.02), blurRadius: 10, offset: const Offset(0, 4)),
],
border: Border.all(color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.2)),
),
child: Column(
children: [
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
leading: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(color: Colors.blue.shade50, borderRadius: BorderRadius.circular(10)),
child: Icon(LucideIcons.downloadCloud, color: Colors.blue.shade600, size: 22),
),
title: const Text('Export Data to CSV', style: TextStyle(fontWeight: FontWeight.w600)),
trailing: _isExporting
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
: const Icon(LucideIcons.chevronRight, size: 20),
onTap: _isExporting ? null : _exportData,
),
Divider(height: 1, indent: 64, color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.05)),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
leading: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(color: Colors.purple.shade50, borderRadius: BorderRadius.circular(10)),
child: Icon(LucideIcons.moon, color: Colors.purple.shade600, size: 22),
),
title: const Text('Theme', style: TextStyle(fontWeight: FontWeight.w600)),
trailing: SegmentedButton<ThemeMode>(
segments: const [
ButtonSegment(value: ThemeMode.light, icon: Icon(LucideIcons.sun, size: 18)),
ButtonSegment(value: ThemeMode.system, icon: Icon(LucideIcons.monitor, size: 18)),
ButtonSegment(value: ThemeMode.dark, icon: Icon(LucideIcons.moon, size: 18)),
],
selected: {ref.watch(themeProvider)},
onSelectionChanged: (Set<ThemeMode> newSelection) {
ref.read(themeProvider.notifier).setTheme(newSelection.first);
},
showSelectedIcon: false,
style: ButtonStyle(
visualDensity: VisualDensity.compact,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
),
Divider(height: 1, indent: 64, color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.05)),
Consumer(
builder: (context, ref, child) {
final isBusinessMode = ref.watch(businessModeProvider);
return Column(
children: [
if (_profileType == 'BUSINESS') ...[
SwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
secondary: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(color: Colors.orange.shade50, borderRadius: BorderRadius.circular(10)),
child: Icon(LucideIcons.briefcase, color: Colors.orange.shade600, size: 22),
),
title: const Text('Business Mode', style: TextStyle(fontWeight: FontWeight.w600)),
subtitle: Text('Inventory and sales', style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
value: isBusinessMode,
activeColor: Theme.of(context).primaryColor,
onChanged: (val) {
ref.read(businessModeProvider.notifier).toggleMode();
},
),
if (isBusinessMode) ...[
Divider(height: 1, indent: 64, color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.05)),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
leading: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10)),
child: Icon(LucideIcons.settings, color: Colors.grey.shade700, size: 22),
),
title: const Text('Business Settings', style: TextStyle(fontWeight: FontWeight.w600)),
subtitle: Text('Tax, Modules, POS', style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
trailing: const Icon(LucideIcons.chevronRight, size: 20),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => const BusinessSettingsScreen()));
},
),
],
Divider(height: 1, indent: 64, color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.05)),
Consumer(
builder: (context, ref, child) {
final isProjectMode = ref.watch(projectModeProvider);
return SwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
secondary: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(color: Colors.teal.shade50, borderRadius: BorderRadius.circular(10)),
child: Icon(LucideIcons.trello, color: Colors.teal.shade600, size: 22),
),
title: const Text('Project Management', style: TextStyle(fontWeight: FontWeight.w600)),
subtitle: Text('Task boards and tracking', style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
value: isProjectMode,
activeColor: Theme.of(context).primaryColor,
onChanged: (val) {
ref.read(projectModeProvider.notifier).toggleMode();
},
);
},
),
Divider(height: 1, indent: 64, color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.05)),
],
],
);
}
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
leading: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(color: Colors.green.shade50, borderRadius: BorderRadius.circular(10)),
child: Icon(LucideIcons.helpCircle, color: Colors.green.shade600, size: 22),
),
title: const Text('Help & Support', style: TextStyle(fontWeight: FontWeight.w600)),
trailing: const Icon(LucideIcons.chevronRight, size: 20),
onTap: () {},
),
],
),
),
icon: const Icon(LucideIcons.logOut),
label: const Text('Log Out', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
const SizedBox(height: 48),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => _logout(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.withOpacity(0.08),
foregroundColor: Colors.red.shade700,
elevation: 0,
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: 0.5)),
),
),
const SizedBox(height: 20),
],
),
const SizedBox(height: 20),
],
),
),
),
),

View File

@@ -74,161 +74,180 @@ class _ResetPasswordScreenState extends ConsumerState<ResetPasswordScreen> {
}
}
InputDecoration _buildInputDecoration(String hint, {IconData? prefixIcon, Widget? suffixIcon}) {
return InputDecoration(
hintText: hint,
prefixIcon: prefixIcon != null ? Icon(prefixIcon, color: Colors.grey.shade500) : null,
suffixIcon: suffixIcon,
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18),
);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
backgroundColor: Colors.grey.shade50,
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.transparent,
leading: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(LucideIcons.chevronLeft, size: 28),
color: Colors.black87,
),
),
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.arrow_back_ios_new, size: 20),
style: IconButton.styleFrom(
backgroundColor: Colors.grey.shade100,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
const SizedBox(height: 32),
Center(
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6C63FF), Color(0xFF48C6EF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
width: 80, height: 80,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
borderRadius: BorderRadius.circular(24),
child: Center(child: Icon(LucideIcons.shieldCheck, size: 40, color: Theme.of(context).primaryColor)),
),
child: const Icon(LucideIcons.shieldCheck, color: Colors.white, size: 36),
),
),
const SizedBox(height: 28),
const Center(
child: Text(
'Reset Password',
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 12),
Center(
child: Text(
'We\'ve sent a 6-digit code to\n${widget.email}',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 15, color: Colors.grey.shade600, height: 1.5),
),
),
const SizedBox(height: 40),
const SizedBox(height: 32),
Text(
'Reset Password',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'We\'ve sent a 6-digit code to\n${widget.email}',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey.shade600, height: 1.5),
),
const SizedBox(height: 40),
// OTP Field
TextField(
controller: otpController,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
maxLength: 6,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 20, letterSpacing: 8),
textAlign: TextAlign.center,
decoration: InputDecoration(
hintText: '000000',
hintStyle: TextStyle(color: Colors.grey.shade300, letterSpacing: 8),
counterText: '',
filled: true,
fillColor: Colors.grey.shade100,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)),
),
),
const SizedBox(height: 20),
// OTP Field
TextField(
controller: otpController,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
maxLength: 6,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 24, letterSpacing: 16),
textAlign: TextAlign.center,
decoration: InputDecoration(
hintText: '000000',
hintStyle: TextStyle(color: Colors.grey.shade300, letterSpacing: 16),
counterText: '',
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300)),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300)),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2)),
),
),
const SizedBox(height: 24),
// New Password
TextField(
controller: newPasswordController,
obscureText: _obscurePassword,
textInputAction: TextInputAction.next,
style: const TextStyle(fontWeight: FontWeight.w500),
decoration: InputDecoration(
hintText: 'New Password',
prefixIcon: Icon(LucideIcons.lock, color: Colors.grey.shade500),
suffixIcon: IconButton(
icon: Icon(_obscurePassword ? LucideIcons.eyeOff : LucideIcons.eye, color: Colors.grey.shade500),
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
// New Password
TextField(
controller: newPasswordController,
obscureText: _obscurePassword,
textInputAction: TextInputAction.next,
style: const TextStyle(fontWeight: FontWeight.w500),
decoration: _buildInputDecoration(
'New Password',
prefixIcon: LucideIcons.lock,
suffixIcon: IconButton(
icon: Icon(_obscurePassword ? LucideIcons.eyeOff : LucideIcons.eye, color: Colors.grey.shade500),
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
),
),
),
filled: true,
fillColor: Colors.grey.shade100,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)),
),
),
const SizedBox(height: 16),
const SizedBox(height: 16),
// Confirm Password
TextField(
controller: confirmPasswordController,
obscureText: _obscureConfirm,
textInputAction: TextInputAction.done,
onSubmitted: (_) => _resetPassword(),
style: const TextStyle(fontWeight: FontWeight.w500),
decoration: InputDecoration(
hintText: 'Confirm Password',
prefixIcon: Icon(LucideIcons.lock, color: Colors.grey.shade500),
suffixIcon: IconButton(
icon: Icon(_obscureConfirm ? LucideIcons.eyeOff : LucideIcons.eye, color: Colors.grey.shade500),
onPressed: () => setState(() => _obscureConfirm = !_obscureConfirm),
// Confirm Password
TextField(
controller: confirmPasswordController,
obscureText: _obscureConfirm,
textInputAction: TextInputAction.done,
onSubmitted: (_) => _resetPassword(),
style: const TextStyle(fontWeight: FontWeight.w500),
decoration: _buildInputDecoration(
'Confirm Password',
prefixIcon: LucideIcons.lock,
suffixIcon: IconButton(
icon: Icon(_obscureConfirm ? LucideIcons.eyeOff : LucideIcons.eye, color: Colors.grey.shade500),
onPressed: () => setState(() => _obscureConfirm = !_obscureConfirm),
),
),
),
filled: true,
fillColor: Colors.grey.shade100,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)),
),
),
if (_errorMessage != null) ...[
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.red.shade200),
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
alignment: Alignment.topCenter,
child: _errorMessage != null
? Padding(
padding: const EdgeInsets.only(top: 16),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.red.shade200),
),
child: Row(
children: [
Icon(LucideIcons.alertCircle, color: Colors.red.shade400, size: 20),
const SizedBox(width: 12),
Expanded(child: Text(_errorMessage!, style: TextStyle(color: Colors.red.shade700, fontSize: 14, fontWeight: FontWeight.w500))),
],
),
),
)
: const SizedBox.shrink(),
),
child: Row(
children: [
Icon(LucideIcons.alertCircle, color: Colors.red.shade400, size: 18),
const SizedBox(width: 10),
Expanded(child: Text(_errorMessage!, style: TextStyle(color: Colors.red.shade700, fontSize: 13))),
],
),
),
],
const SizedBox(height: 32),
const SizedBox(height: 40),
SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: _isLoading ? null : _resetPassword,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6C63FF),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 0,
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _resetPassword,
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 20),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 2,
),
child: _isLoading
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
: const Text('Reset Password', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 0.5)),
),
),
child: _isLoading
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
: const Text('Reset Password', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
],
),
],
),
),
),
),

View File

@@ -0,0 +1,501 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:intl_phone_field/intl_phone_field.dart';
import '../../../../core/network/dio_client.dart';
import '../../dashboard/presentation/dashboard_screen.dart';
import 'package:url_launcher/url_launcher.dart';
class SetupWizardScreen extends StatefulWidget {
const SetupWizardScreen({super.key});
@override
State<SetupWizardScreen> createState() => _SetupWizardScreenState();
}
class _SetupWizardScreenState extends State<SetupWizardScreen> with SingleTickerProviderStateMixin {
bool _isLoading = false;
int _currentStep = 0;
String _accountType = 'INDIVIDUAL';
// Step 1: Personal Info
final _nameController = TextEditingController();
final _usernameController = TextEditingController();
String _mobileNumber = '';
bool _isUsernameAvailable = false;
bool _checkingUsername = false;
// Step 2: Business Info (if BUSINESS)
String? _natureOfBusiness;
final _businessNameController = TextEditingController();
final _addressController = TextEditingController();
final _emailController = TextEditingController();
final _gstController = TextEditingController();
final _panController = TextEditingController();
final _msmeController = TextEditingController();
// Step 3: Consent
bool _termsAccepted = false;
bool _privacyAccepted = false;
int get _totalSteps => _accountType == 'BUSINESS' ? 4 : 3;
Future<void> _checkUsername(String username) async {
if (username.length < 3) return;
setState(() => _checkingUsername = true);
try {
final res = await DioClient().dio.get('/account/username/availability', queryParameters: {'username': username});
if (mounted) {
setState(() {
_isUsernameAvailable = res.data['available'] == true;
_checkingUsername = false;
});
}
} catch (e) {
if (mounted) setState(() => _checkingUsername = false);
}
}
Future<void> _submitSetup() async {
if (!_termsAccepted || !_privacyAccepted) return;
setState(() => _isLoading = true);
try {
final data = {
'accountType': _accountType,
'name': _nameController.text,
'username': _usernameController.text,
'mobileNumber': _mobileNumber,
'termsAccepted': _termsAccepted,
'privacyAccepted': _privacyAccepted,
};
if (_accountType == 'BUSINESS') {
data.addAll({
'businessName': _businessNameController.text.isNotEmpty ? _businessNameController.text : _nameController.text,
'natureOfBusiness': _natureOfBusiness!,
'address': _addressController.text,
'emailId': _emailController.text,
'gstin': _gstController.text,
'panNumber': _panController.text,
'msmeNumber': _msmeController.text,
});
}
await DioClient().dio.post('/account/setup', data: data);
if (mounted) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const DashboardScreen()));
}
} catch (e) {
if (mounted) setState(() => _isLoading = false);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Setup failed: $e')));
}
}
}
Future<void> _launchUrl(String path) async {
final url = Uri.parse('http://192.168.0.104:8080/public/legal/$path');
if (await canLaunchUrl(url)) {
await launchUrl(url);
}
}
void _nextStep() {
if (_currentStep == 1) {
if (_nameController.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please enter your Full Name')));
return;
}
if (_usernameController.text.trim().length < 3) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Username must be at least 3 characters')));
return;
}
if (!_isUsernameAvailable) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Username is unavailable or invalid')));
return;
}
}
if (_accountType == 'BUSINESS' && _currentStep == 2) {
if (_natureOfBusiness == null) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please select Nature of Business')));
return;
}
}
if (_currentStep < _totalSteps - 1) {
setState(() => _currentStep++);
} else {
_submitSetup();
}
}
void _prevStep() {
if (_currentStep > 0) {
setState(() => _currentStep--);
}
}
InputDecoration _buildInputDecoration(String label) {
return InputDecoration(
labelText: label,
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18),
);
}
Widget _buildProgressIndicator() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Step ${_currentStep + 1} of $_totalSteps',
style: TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold, fontSize: 14),
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: (_currentStep + 1) / _totalSteps,
minHeight: 6,
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1),
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
),
),
],
),
);
}
Widget _buildStep0AccountType() {
return Column(
key: const ValueKey(0),
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text('Welcome to Kifi!', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
const Text('How will you be using the application?', style: TextStyle(fontSize: 16, color: Colors.grey)),
const SizedBox(height: 32),
_buildAccountTypeCard('INDIVIDUAL', 'Individual', 'For personal use and expenses', LucideIcons.user),
const SizedBox(height: 16),
_buildAccountTypeCard('BUSINESS', 'Business', 'For managing business and inventory', LucideIcons.building2),
],
);
}
Widget _buildAccountTypeCard(String type, String title, String subtitle, IconData icon) {
final isSelected = _accountType == type;
return InkWell(
onTap: () => setState(() => _accountType = type),
borderRadius: BorderRadius.circular(16),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isSelected ? Theme.of(context).primaryColor.withOpacity(0.08) : Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected ? Theme.of(context).primaryColor : Colors.grey.shade300,
width: isSelected ? 2 : 1,
),
boxShadow: isSelected ? [] : [
BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 10, offset: const Offset(0, 4)),
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected ? Theme.of(context).primaryColor : Colors.grey.shade100,
shape: BoxShape.circle,
),
child: Icon(icon, color: isSelected ? Colors.white : Colors.grey[600], size: 28),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(subtitle, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
],
),
),
Icon(
isSelected ? LucideIcons.checkCircle2 : LucideIcons.circle,
color: isSelected ? Theme.of(context).primaryColor : Colors.grey[300],
size: 28,
),
],
),
),
);
}
Widget _buildStep1Personal() {
return Column(
key: const ValueKey(1),
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text('Personal Details', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
const Text('Tell us a bit about yourself.', style: TextStyle(fontSize: 16, color: Colors.grey)),
const SizedBox(height: 32),
TextFormField(
controller: _nameController,
decoration: _buildInputDecoration('Full Name *'),
textInputAction: TextInputAction.next,
),
const SizedBox(height: 20),
TextFormField(
controller: _usernameController,
decoration: _buildInputDecoration('Username *').copyWith(
suffixIcon: SizedBox(
width: 48, height: 48,
child: Center(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: _checkingUsername
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
: Icon(
_isUsernameAvailable ? LucideIcons.checkCircle2 : LucideIcons.xCircle,
key: ValueKey(_isUsernameAvailable),
color: _usernameController.text.isEmpty ? Colors.transparent : (_isUsernameAvailable ? Colors.green : Colors.red),
),
),
),
),
),
onChanged: _checkUsername,
textInputAction: TextInputAction.next,
),
const SizedBox(height: 20),
IntlPhoneField(
decoration: _buildInputDecoration('Mobile Number'),
initialCountryCode: 'IN',
onChanged: (phone) => _mobileNumber = phone.completeNumber,
),
],
);
}
Widget _buildStep2Business() {
return Column(
key: const ValueKey(2),
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text('Business Profile', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
const Text('Set up your company details for invoicing and tracking.', style: TextStyle(fontSize: 16, color: Colors.grey)),
const SizedBox(height: 32),
DropdownButtonFormField<String>(
decoration: _buildInputDecoration('Nature of Business *'),
value: _natureOfBusiness,
items: const [
DropdownMenuItem(value: 'JEWELLERY', child: Text('Jewellery')),
DropdownMenuItem(value: 'PROJECT_MANAGEMENT', child: Text('Project Management')),
DropdownMenuItem(value: 'INVENTORY_MANAGEMENT', child: Text('Inventory Management')),
],
onChanged: (val) => setState(() => _natureOfBusiness = val),
),
const SizedBox(height: 20),
TextFormField(
controller: _businessNameController,
decoration: _buildInputDecoration('Business Name (Optional)'),
textInputAction: TextInputAction.next,
),
const SizedBox(height: 20),
TextFormField(
controller: _addressController,
decoration: _buildInputDecoration('Business Address (Optional)'),
maxLines: 3,
textInputAction: TextInputAction.newline,
),
const SizedBox(height: 20),
TextFormField(
controller: _emailController,
decoration: _buildInputDecoration('Official Email (Optional)'),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
),
const SizedBox(height: 20),
TextFormField(
controller: _gstController,
decoration: _buildInputDecoration('GST Number (Optional)'),
textCapitalization: TextCapitalization.characters,
textInputAction: TextInputAction.next,
),
const SizedBox(height: 20),
TextFormField(
controller: _panController,
decoration: _buildInputDecoration('PAN (Optional)'),
textCapitalization: TextCapitalization.characters,
textInputAction: TextInputAction.next,
),
const SizedBox(height: 20),
TextFormField(
controller: _msmeController,
decoration: _buildInputDecoration('MSME/Udyam Number (Optional)'),
textCapitalization: TextCapitalization.characters,
textInputAction: TextInputAction.done,
),
],
);
}
Widget _buildStep3Consent() {
return Column(
key: const ValueKey(3),
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text('Final Step', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
const Text('Please review and accept our policies to continue.', style: TextStyle(fontSize: 16, color: Colors.grey)),
const SizedBox(height: 32),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade300),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.02), blurRadius: 10, offset: const Offset(0, 4)),
],
),
child: Column(
children: [
CheckboxListTile(
title: InkWell(
onTap: () => _launchUrl('terms'),
child: const Text('I accept the Terms & Conditions', style: TextStyle(color: Colors.blue, decoration: TextDecoration.underline, fontSize: 15, fontWeight: FontWeight.w500)),
),
value: _termsAccepted,
controlAffinity: ListTileControlAffinity.leading,
activeColor: Theme.of(context).primaryColor,
onChanged: (val) => setState(() => _termsAccepted = val ?? false),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
Divider(height: 1, indent: 56, color: Colors.grey.shade200),
CheckboxListTile(
title: InkWell(
onTap: () => _launchUrl('privacy'),
child: const Text('I accept the Privacy Policy', style: TextStyle(color: Colors.blue, decoration: TextDecoration.underline, fontSize: 15, fontWeight: FontWeight.w500)),
),
value: _privacyAccepted,
controlAffinity: ListTileControlAffinity.leading,
activeColor: Theme.of(context).primaryColor,
onChanged: (val) => setState(() => _privacyAccepted = val ?? false),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
],
),
),
],
);
}
Widget _buildCurrentStep() {
if (_currentStep == 0) return _buildStep0AccountType();
if (_currentStep == 1) return _buildStep1Personal();
if (_accountType == 'BUSINESS' && _currentStep == 2) return _buildStep2Business();
return _buildStep3Consent(); // Step 2 (Individual) or Step 3 (Business)
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.shade50,
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.transparent,
leading: _currentStep > 0
? IconButton(icon: const Icon(LucideIcons.chevronLeft), onPressed: _prevStep)
: null,
),
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildProgressIndicator(),
Expanded(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
transitionBuilder: (child, animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.05, 0),
end: Offset.zero,
).animate(animation),
child: FadeTransition(
opacity: animation,
child: child,
),
);
},
child: SingleChildScrollView(
key: ValueKey(_currentStep),
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
child: _buildCurrentStep(),
),
),
),
),
),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 24.0),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.grey.shade200)),
),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _nextStep,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 20),
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
child: _isLoading
? const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
: Text(
_currentStep == _totalSteps - 1 ? 'Complete Setup' : 'Continue',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 0.5),
),
),
),
),
),
),
],
),
),
);
}
}

View File

@@ -24,6 +24,7 @@ class _CategorySpendingChartState extends State<CategorySpendingChart> {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final Map<int, double> categoryTotals = {};
for (var t in widget.transactions) {
bool matches = false;
@@ -74,9 +75,9 @@ class _CategorySpendingChartState extends State<CategorySpendingChart> {
? Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.white,
color: Theme.of(context).cardColor,
shape: BoxShape.circle,
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 4)],
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.1), blurRadius: 4)],
),
child: Icon(Icons.touch_app, size: 16, color: colors[i % colors.length]),
)
@@ -88,21 +89,21 @@ class _CategorySpendingChartState extends State<CategorySpendingChart> {
return Card(
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(32), side: BorderSide(color: Colors.grey.shade200)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(32), side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.2))),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.white, Colors.grey.shade50],
colors: isDark ? [Theme.of(context).cardColor, Theme.of(context).cardColor.withOpacity(0.8)] : [Colors.white, Colors.grey.shade50],
),
),
padding: const EdgeInsets.all(28.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Category Breakdown', style: TextStyle(fontSize: 22, fontWeight: FontWeight.w800, letterSpacing: -0.5)),
Text('Category Breakdown', style: TextStyle(fontSize: 22, fontWeight: FontWeight.w800, letterSpacing: -0.5, color: Theme.of(context).textTheme.bodyLarge?.color)),
const SizedBox(height: 32),
SizedBox(
@@ -134,7 +135,7 @@ class _CategorySpendingChartState extends State<CategorySpendingChart> {
mainAxisSize: MainAxisSize.min,
children: [
Text('Total', style: TextStyle(fontSize: 14, color: Colors.grey.shade500, fontWeight: FontWeight.w600)),
Text('Rs. ${totalAmount.toStringAsFixed(0)}', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900)),
Text('Rs. ${totalAmount.toStringAsFixed(0)}', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: Theme.of(context).textTheme.bodyLarge?.color)),
],
),
],
@@ -142,7 +143,7 @@ class _CategorySpendingChartState extends State<CategorySpendingChart> {
),
const SizedBox(height: 40),
const Text('Details', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Colors.black87)),
Text('Details', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Theme.of(context).textTheme.bodyLarge?.color)),
const SizedBox(height: 16),
// Modern List for Legends
@@ -162,10 +163,10 @@ class _CategorySpendingChartState extends State<CategorySpendingChart> {
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
decoration: BoxDecoration(
color: isTouched ? colors[i % colors.length].withValues(alpha: 0.05) : Colors.white,
color: isTouched ? colors[i % colors.length].withValues(alpha: 0.05) : (isDark ? Theme.of(context).scaffoldBackgroundColor : Colors.white),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isTouched ? colors[i % colors.length].withValues(alpha: 0.3) : Colors.grey.shade100,
color: isTouched ? colors[i % colors.length].withValues(alpha: 0.3) : Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.05),
width: isTouched ? 1.5 : 1.0,
),
boxShadow: [
@@ -195,7 +196,7 @@ class _CategorySpendingChartState extends State<CategorySpendingChart> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(categoryName, style: TextStyle(fontWeight: FontWeight.w700, fontSize: 15, color: Colors.grey.shade800)),
Text(categoryName, style: TextStyle(fontWeight: FontWeight.w700, fontSize: 15, color: Theme.of(context).textTheme.bodyLarge?.color)),
const SizedBox(height: 4),
Text('Rs. ${amount.toStringAsFixed(0)}', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13, color: Colors.grey.shade500)),
],

View File

@@ -39,9 +39,10 @@ class _StatisticsTabState extends State<StatisticsTab> {
String _selectedNature = 'EXPENSE';
Widget _buildTotalCard(String title, double amount, Color color, IconData icon) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Card(
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16), side: BorderSide(color: Colors.grey.shade200)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16), side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.2))),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
@@ -171,7 +172,7 @@ class _StatisticsTabState extends State<StatisticsTab> {
if (widget.transactions.isEmpty)
Card(
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24), side: BorderSide(color: Colors.grey.shade200)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24), side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(Theme.of(context).brightness == Brightness.dark ? 0.1 : 0.2))),
child: const Padding(
padding: EdgeInsets.all(32.0),
child: Center(child: Text('No transactions in this period.', style: TextStyle(color: Colors.grey))),

View File

@@ -49,6 +49,7 @@ class _SwipeableAccountCardState extends State<SwipeableAccountCard> {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
if (widget.items.isEmpty) {
return const SizedBox.shrink();
}
@@ -73,11 +74,11 @@ class _SwipeableAccountCardState extends State<SwipeableAccountCard> {
margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade200),
border: Border.all(color: Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.2)),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.02), blurRadius: 8, offset: const Offset(0, 2))
BoxShadow(color: Colors.black.withOpacity(isDark ? 0.2 : 0.02), blurRadius: 8, offset: const Offset(0, 2))
],
),
child: Row(
@@ -120,7 +121,7 @@ class _SwipeableAccountCardState extends State<SwipeableAccountCard> {
),
Text(
'Rs. ${item.amount.toStringAsFixed(0)}',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Colors.black87),
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Theme.of(context).textTheme.bodyLarge?.color),
),
],
),

View File

@@ -11,6 +11,7 @@ class UpcomingDuesWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final walletsState = ref.watch(walletProvider);
if (!walletsState.hasValue || walletsState.value == null) {
@@ -126,11 +127,11 @@ class UpcomingDuesWidget extends ConsumerWidget {
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isUrgent ? Colors.red.shade50 : Colors.white,
color: isUrgent ? (isDark ? Colors.red.withOpacity(0.2) : Colors.red.shade50) : Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: isUrgent ? Colors.red.shade200 : Colors.grey.shade200),
border: Border.all(color: isUrgent ? (isDark ? Colors.red.withOpacity(0.5) : Colors.red.shade200) : Theme.of(context).dividerColor.withOpacity(isDark ? 0.1 : 0.2)),
boxShadow: [
if (!isUrgent) BoxShadow(color: Colors.black.withOpacity(0.02), blurRadius: 8, offset: const Offset(0, 2))
if (!isUrgent) BoxShadow(color: Colors.black.withOpacity(isDark ? 0.2 : 0.02), blurRadius: 8, offset: const Offset(0, 2))
],
),
child: Row(
@@ -153,7 +154,7 @@ class UpcomingDuesWidget extends ConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(w.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15)),
Text(w.name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15, color: Theme.of(context).textTheme.bodyLarge?.color)),
const SizedBox(height: 4),
Text(
'$label${DateFormat('MMM dd').format(dueDate)}',
@@ -171,7 +172,7 @@ class UpcomingDuesWidget extends ConsumerWidget {
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: isUrgent ? Colors.red.shade700 : Colors.black87
color: isUrgent ? (isDark ? Colors.redAccent : Colors.red.shade700) : Theme.of(context).textTheme.bodyLarge?.color
),
),
],

View File

@@ -163,7 +163,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(28),
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 10))],
),
@@ -180,7 +180,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
decoration: InputDecoration(
labelText: 'Item Name',
filled: true,
fillColor: Colors.grey.shade100,
fillColor: Theme.of(context).brightness == Brightness.dark ? Colors.grey.shade900 : Colors.grey.shade100,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)),
)
@@ -194,9 +194,9 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
decoration: InputDecoration(
labelText: 'Amount',
prefixText: 'Rs. ',
prefixStyle: const TextStyle(color: Colors.black87, fontWeight: FontWeight.w500, fontSize: 16),
prefixStyle: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color, fontWeight: FontWeight.w500, fontSize: 16),
filled: true,
fillColor: Colors.grey.shade100,
fillColor: Theme.of(context).brightness == Brightness.dark ? Colors.grey.shade900 : Colors.grey.shade100,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)),
)
@@ -260,7 +260,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(28),
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 10))],
),
@@ -287,7 +287,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
decoration: InputDecoration(
hintText: 'Account Name (e.g. Household)',
filled: true,
fillColor: Colors.grey.shade100,
fillColor: Theme.of(context).brightness == Brightness.dark ? Colors.grey.shade900 : Colors.grey.shade100,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)),
),
@@ -299,7 +299,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
decoration: InputDecoration(
labelText: 'Account Nature',
filled: true,
fillColor: Colors.grey.shade100,
fillColor: Theme.of(context).brightness == Brightness.dark ? Colors.grey.shade900 : Colors.grey.shade100,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)),
),
@@ -482,7 +482,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(28),
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 10))],
),
@@ -500,7 +500,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
decoration: InputDecoration(
hintText: 'Name',
filled: true,
fillColor: Colors.grey.shade100,
fillColor: Theme.of(context).brightness == Brightness.dark ? Colors.grey.shade900 : Colors.grey.shade100,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)),
),
@@ -749,7 +749,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
hintText: '0.00',
hintStyle: TextStyle(color: Colors.grey.shade400),
prefixText: 'Rs. ',
prefixStyle: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87),
prefixStyle: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Theme.of(context).textTheme.bodyLarge?.color),
filled: true,
fillColor: const Color(0xFF6C63FF).withValues(alpha: 0.05),
contentPadding: const EdgeInsets.symmetric(vertical: 24, horizontal: 24),
@@ -766,7 +766,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
hintText: 'Description (Optional)',
prefixIcon: const Icon(LucideIcons.alignLeft, color: Colors.grey),
filled: true,
fillColor: Colors.grey.shade100,
fillColor: Theme.of(context).brightness == Brightness.dark ? Colors.grey.shade900 : Colors.grey.shade100,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)),