V1 - Personal Account and Budgeting Done
Personal Account and Budgeting
This commit is contained in:
@@ -0,0 +1,881 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../transactions/providers/providers.dart';
|
||||
import '../../transactions/data/models.dart';
|
||||
import '../../transactions/data/repository.dart';
|
||||
import 'wallet_ledger_screen.dart';
|
||||
|
||||
class AccountsScreen extends ConsumerStatefulWidget {
|
||||
final String? initialFilterNature;
|
||||
const AccountsScreen({super.key, this.initialFilterNature});
|
||||
|
||||
@override
|
||||
ConsumerState<AccountsScreen> createState() => _AccountsScreenState();
|
||||
}
|
||||
|
||||
class _AccountsScreenState extends ConsumerState<AccountsScreen> {
|
||||
String _searchQuery = '';
|
||||
String? _filterNature;
|
||||
bool _isSearching = false;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_filterNature = widget.initialFilterNature;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _onRefresh() async {
|
||||
ref.invalidate(walletProvider);
|
||||
ref.invalidate(invitationProvider);
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
}
|
||||
|
||||
void _showCreateWalletDialog() {
|
||||
final ctrl = TextEditingController();
|
||||
final amtCtrl = TextEditingController();
|
||||
DateTime openingDate = DateTime.now();
|
||||
String selectedNature = 'CASH';
|
||||
final natures = ['CASH', 'SAVINGS', 'INVESTMENTS', 'LOAN', 'LENDING', 'PAYABLES', 'INCOME', 'EXPENSE', 'RECEIVABLES'];
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (context, setStateDialog) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
child: GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF6C63FF).withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(LucideIcons.wallet, color: Color(0xFF6C63FF), size: 24),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const Text(
|
||||
'New Account',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
TextField(
|
||||
controller: ctrl,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Account Name (e.g. Household)',
|
||||
filled: true,
|
||||
fillColor: 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)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
DropdownButtonFormField<String>(
|
||||
value: selectedNature,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.black87),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Account Nature',
|
||||
filled: true,
|
||||
fillColor: 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)),
|
||||
),
|
||||
items: natures.map((n) => DropdownMenuItem(value: n, child: Text(n))).toList(),
|
||||
onChanged: (val) {
|
||||
if (val != null) setStateDialog(() => selectedNature = val);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextField(
|
||||
controller: amtCtrl,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: false),
|
||||
textInputAction: TextInputAction.done,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Opening Balance (Optional)',
|
||||
prefixText: 'Rs. ',
|
||||
prefixStyle: const TextStyle(color: Colors.black87, fontWeight: FontWeight.w500, fontSize: 16),
|
||||
filled: true,
|
||||
fillColor: 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)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: openingDate,
|
||||
firstDate: DateTime.now().subtract(const Duration(days: 365 * 10)),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
if (picked != null) {
|
||||
setStateDialog(() => openingDate = picked);
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(LucideIcons.calendar, color: Colors.grey),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Date: ${DateFormat('MMM dd, yyyy').format(openingDate)}',
|
||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.grey.shade700,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: const Text('Cancel', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (ctrl.text.isNotEmpty) {
|
||||
final double amt = double.tryParse(amtCtrl.text) ?? 0.0;
|
||||
final newWallet = await ref.read(walletProvider.notifier).createWallet(
|
||||
name: ctrl.text,
|
||||
nature: selectedNature,
|
||||
initialBalance: 0.0,
|
||||
);
|
||||
|
||||
if (amt > 0) {
|
||||
final tx = Transaction(
|
||||
id: 0,
|
||||
type: 'INCOME',
|
||||
amount: amt,
|
||||
date: openingDate,
|
||||
description: 'Opening Balance',
|
||||
toWalletId: newWallet.id,
|
||||
);
|
||||
await ref.read(transactionProvider.notifier).addTransaction(tx);
|
||||
}
|
||||
|
||||
if (context.mounted) Navigator.pop(ctx);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF6C63FF),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text('Create', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showFilterSheet() {
|
||||
final natures = ['ALL', 'CASH', 'SAVINGS', 'INVESTMENTS', 'LOAN', 'LENDING', 'PAYABLES', 'INCOME', 'EXPENSE', 'RECEIVABLES'];
|
||||
String? tempFilterNature = _filterNature;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (context, setSheetState) {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Filter Accounts', style: Theme.of(context).textTheme.titleLarge),
|
||||
IconButton(icon: const Icon(LucideIcons.x), onPressed: () => Navigator.pop(ctx)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
Text('Account Nature', style: Theme.of(context).textTheme.titleSmall),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<String>(
|
||||
value: tempFilterNature ?? 'ALL',
|
||||
style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.black87, fontSize: 16),
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade100,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
|
||||
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)),
|
||||
),
|
||||
items: natures.map((n) => DropdownMenuItem(value: n, child: Text(n))).toList(),
|
||||
onChanged: (val) {
|
||||
if (val != null) {
|
||||
setSheetState(() {
|
||||
tempFilterNature = val == 'ALL' ? null : val;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
setState(() => _filterNature = null);
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Text('Reset', style: TextStyle(color: Colors.grey.shade700, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() => _filterNature = tempFilterNature);
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: const Color(0xFF6C63FF),
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text('Apply Filter', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final walletsState = ref.watch(walletProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
if (Navigator.of(context).canPop())
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: IconButton(
|
||||
icon: const Icon(LucideIcons.arrowLeft),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
onChanged: (val) => setState(() => _searchQuery = val),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search accounts...',
|
||||
prefixIcon: const Icon(LucideIcons.search),
|
||||
suffixIcon: _searchController.text.isNotEmpty ? IconButton(
|
||||
icon: const Icon(LucideIcons.x),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
setState(() => _searchQuery = '');
|
||||
},
|
||||
) : null,
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade100,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
LucideIcons.filter,
|
||||
color: _filterNature != null ? const Color(0xFF6C63FF) : null,
|
||||
),
|
||||
onPressed: _showFilterSheet,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(LucideIcons.plusCircle),
|
||||
onPressed: _showCreateWalletDialog,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: walletsState.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, st) => Center(child: Text('Error: $e')),
|
||||
data: (allWallets) {
|
||||
final wallets = allWallets.where((w) {
|
||||
final matchSearch = _searchQuery.isEmpty ||
|
||||
w.name.toLowerCase().contains(_searchQuery.toLowerCase());
|
||||
final matchNature = _filterNature == null || w.nature == _filterNature;
|
||||
return matchSearch && matchNature;
|
||||
}).toList();
|
||||
|
||||
if (wallets.isEmpty) {
|
||||
return const Center(child: Text('No accounts found.'));
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _onRefresh,
|
||||
child: ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: wallets.length,
|
||||
itemBuilder: (context, index) {
|
||||
final w = wallets[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16), side: BorderSide(color: Colors.grey.shade200)),
|
||||
elevation: 0,
|
||||
child: Column(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (_) => WalletLedgerScreen(wallet: w)));
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: Colors.blue.withValues(alpha: 0.1),
|
||||
child: const Icon(LucideIcons.wallet, color: Colors.blue),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(w.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
const SizedBox(height: 4),
|
||||
Text('${w.nature ?? "CASH"} • Balance: Rs. ${w.balance}', style: TextStyle(color: Colors.grey.shade600, fontSize: 13)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Divider(height: 1, color: Colors.grey.shade200),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
icon: const Icon(LucideIcons.userPlus, size: 14),
|
||||
label: const Text('Invite', style: TextStyle(fontSize: 12)),
|
||||
style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 8)),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => InviteDialogWidget(wallet: w),
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton.icon(
|
||||
icon: const Icon(LucideIcons.edit2, size: 14),
|
||||
label: const Text('Edit', style: TextStyle(fontSize: 12)),
|
||||
style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 8)),
|
||||
onPressed: () {
|
||||
final editCtrl = TextEditingController(text: w.name);
|
||||
String editNature = w.nature ?? 'CASH';
|
||||
final natures = ['CASH', 'SAVINGS', 'EXPENSE', 'INVESTMENTS', 'LOAN', 'LENDING', 'PAYABLES', 'RECEIVABLES', 'INCOME'];
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (context, setStateDialog) => Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(28)),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Edit Account', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 24),
|
||||
TextField(
|
||||
controller: editCtrl,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Account Name',
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade100,
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<String>(
|
||||
value: editNature,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Account Nature',
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade100,
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
|
||||
),
|
||||
items: natures.map((n) => DropdownMenuItem(value: n, child: Text(n))).toList(),
|
||||
onChanged: (val) {
|
||||
if (val != null) setStateDialog(() => editNature = val);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('Cancel', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (editCtrl.text.isNotEmpty) {
|
||||
try {
|
||||
await ref.read(walletProvider.notifier).editWallet(
|
||||
w.id,
|
||||
name: editCtrl.text.trim(),
|
||||
nature: editNature,
|
||||
);
|
||||
if (context.mounted) {
|
||||
Navigator.pop(ctx);
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Wallet updated')));
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to edit: $e')));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF6C63FF), foregroundColor: Colors.white),
|
||||
child: const Text('Save', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton.icon(
|
||||
icon: const Icon(LucideIcons.trash2, size: 14, color: Colors.red),
|
||||
label: const Text('Delete', style: TextStyle(color: Colors.red, fontSize: 12)),
|
||||
style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 8)),
|
||||
onPressed: () async {
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(28)),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Delete Account', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.red)),
|
||||
const SizedBox(height: 16),
|
||||
Text('Are you sure you want to delete ${w.name}? This action cannot be undone.', style: const TextStyle(fontSize: 16)),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('Cancel', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))),
|
||||
child: const Text('Delete', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (confirm == true) {
|
||||
try {
|
||||
await ref.read(walletProvider.notifier).deleteWallet(w.id);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Wallet deleted')));
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString().replaceAll('Exception: ', ''))));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
TextButton.icon(
|
||||
icon: const Icon(LucideIcons.list, size: 14),
|
||||
label: const Text('Ledger', style: TextStyle(fontSize: 12)),
|
||||
style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 8)),
|
||||
onPressed: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (_) => WalletLedgerScreen(wallet: w)));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
), // Close FittedBox
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InviteDialogWidget extends ConsumerStatefulWidget {
|
||||
final Wallet wallet;
|
||||
const InviteDialogWidget({super.key, required this.wallet});
|
||||
|
||||
@override
|
||||
ConsumerState<InviteDialogWidget> createState() => _InviteDialogWidgetState();
|
||||
}
|
||||
|
||||
class _InviteDialogWidgetState extends ConsumerState<InviteDialogWidget> {
|
||||
TextEditingController _autoCompleteCtrl = TextEditingController();
|
||||
bool _isInviting = false;
|
||||
bool _isLoadingMembers = true;
|
||||
List<WalletMember> _members = [];
|
||||
List<String> _knownContacts = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchMembers();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_autoCompleteCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _fetchMembers() async {
|
||||
try {
|
||||
final members = await ApiRepository().getWalletMembers(widget.wallet.id);
|
||||
|
||||
List<String> contacts = [];
|
||||
try {
|
||||
contacts = await ApiRepository().getKnownContacts();
|
||||
} catch (e) {
|
||||
debugPrint("Error fetching contacts: $e");
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_members = members;
|
||||
_knownContacts = contacts.where((c) => !members.any((m) => m.email == c)).toList();
|
||||
_isLoadingMembers = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _isLoadingMembers = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _removeMember(WalletMember member) async {
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Remove Member'),
|
||||
content: Text('Are you sure you want to remove ${member.email} from this wallet?'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel')),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red, foregroundColor: Colors.white),
|
||||
child: const Text('Remove'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirm == true) {
|
||||
try {
|
||||
await ApiRepository().removeWalletMember(widget.wallet.id, member.userId);
|
||||
await _fetchMembers();
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Member removed.')));
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString().replaceAll('Exception: ', ''))));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
child: GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 10))],
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Invite to ${widget.wallet.name}', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 24),
|
||||
TextField(
|
||||
controller: _autoCompleteCtrl,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
textInputAction: TextInputAction.done,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
onChanged: (val) {
|
||||
setState(() {});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Enter Email Address to invite',
|
||||
filled: true,
|
||||
fillColor: 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)),
|
||||
),
|
||||
),
|
||||
if (_autoCompleteCtrl?.text.trim().isNotEmpty == true && (_autoCompleteCtrl?.text.length ?? 0) >= 2)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final query = _autoCompleteCtrl.text.trim().toLowerCase();
|
||||
final matches = _knownContacts.where((c) => c.toLowerCase().contains(query)).toList();
|
||||
if (matches.isEmpty || (matches.length == 1 && matches.first.toLowerCase() == query)) return const SizedBox.shrink();
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
constraints: const BoxConstraints(maxHeight: 160),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 4))],
|
||||
),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: matches.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1, indent: 16, endIndent: 16),
|
||||
itemBuilder: (ctx, index) {
|
||||
final email = matches[index];
|
||||
return ListTile(
|
||||
leading: const CircleAvatar(radius: 14, backgroundColor: Color(0xFF6C63FF), child: Icon(LucideIcons.user, size: 14, color: Colors.white)),
|
||||
title: Text(email, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_autoCompleteCtrl.text = email;
|
||||
_autoCompleteCtrl.selection = TextSelection.fromPosition(TextPosition(offset: email.length));
|
||||
});
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _isInviting ? null : () => Navigator.pop(context),
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.grey.shade700, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
|
||||
child: const Text('Cancel', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ElevatedButton(
|
||||
onPressed: _isInviting ? null : () async {
|
||||
final email = _autoCompleteCtrl.text.trim();
|
||||
if (email.isNotEmpty && email.contains('@')) {
|
||||
setState(() => _isInviting = true);
|
||||
try {
|
||||
await ref.read(walletProvider.notifier).inviteUser(widget.wallet.id, email);
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('User invited!')));
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
setState(() => _isInviting = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString().replaceAll('Exception: ', ''))));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF6C63FF),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
elevation: 0,
|
||||
),
|
||||
child: _isInviting
|
||||
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||
: const Text('Invite', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text('Existing Members', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 12),
|
||||
_isLoadingMembers
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _members.isEmpty
|
||||
? const Text('No members found.', style: TextStyle(color: Colors.grey))
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: _members.length,
|
||||
itemBuilder: (context, index) {
|
||||
final member = _members[index];
|
||||
final isOwner = member.role == 'OWNER';
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: CircleAvatar(backgroundColor: Colors.grey.shade200, child: const Icon(LucideIcons.user, color: Colors.grey)),
|
||||
title: Text(member.email, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
|
||||
subtitle: Text(member.role, style: TextStyle(color: isOwner ? Colors.blue : Colors.grey, fontSize: 12)),
|
||||
trailing: !isOwner
|
||||
? IconButton(
|
||||
icon: const Icon(LucideIcons.userMinus, color: Colors.red, size: 20),
|
||||
onPressed: () => _removeMember(member),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user