first commit
This commit is contained in:
769
lib/Screens/party ledger/ledger_party_list_screen.dart
Normal file
769
lib/Screens/party ledger/ledger_party_list_screen.dart
Normal file
@@ -0,0 +1,769 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:hugeicons/hugeicons.dart';
|
||||
import 'package:mobile_pos/Const/api_config.dart';
|
||||
import 'package:mobile_pos/Screens/Customers/Provider/customer_provider.dart';
|
||||
import 'package:mobile_pos/Screens/party%20ledger/single_party_ledger_screen.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:mobile_pos/widgets/empty_widget/_empty_widget.dart';
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../Provider/profile_provider.dart';
|
||||
import '../../currency.dart';
|
||||
import '../../pdf_report/ledger_report_pdf/customer_ledger_report_pdf.dart';
|
||||
import '../../pdf_report/ledger_report_pdf/supplier_ledger_report_pdf.dart';
|
||||
import '../../service/check_user_role_permission_provider.dart';
|
||||
import '../Customers/Model/parties_model.dart';
|
||||
|
||||
class LedgerPartyListScreen extends StatefulWidget {
|
||||
const LedgerPartyListScreen({
|
||||
super.key,
|
||||
this.isReport = false,
|
||||
this.type,
|
||||
});
|
||||
final bool isReport;
|
||||
final String? type;
|
||||
|
||||
@override
|
||||
State<LedgerPartyListScreen> createState() => _LedgerPartyListScreenState();
|
||||
}
|
||||
|
||||
class _LedgerPartyListScreenState extends State<LedgerPartyListScreen> {
|
||||
bool _isRefreshing = false;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
String _searchText = '';
|
||||
Future<void> refreshData(WidgetRef ref) async {
|
||||
if (_isRefreshing) return;
|
||||
_isRefreshing = true;
|
||||
ref.refresh(partiesProvider);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
_isRefreshing = false;
|
||||
}
|
||||
|
||||
List<String> get availablePartyTypes {
|
||||
if (widget.isReport) {
|
||||
// Report mode
|
||||
if (widget.type == 'supplier') {
|
||||
return ['Supplier'];
|
||||
}
|
||||
return ['All Party', 'Customer', 'Dealer', 'Wholesaler'];
|
||||
}
|
||||
return ['All', 'Customer', 'Supplier', 'Dealer', 'Wholesaler'];
|
||||
}
|
||||
|
||||
String getPartyLegerTypeLabel(String value) {
|
||||
switch (value) {
|
||||
case 'All':
|
||||
return lang.S.current.all;
|
||||
case 'All Party':
|
||||
return lang.S.current.allParty;
|
||||
case 'Customer':
|
||||
return lang.S.current.customer;
|
||||
case 'Supplier':
|
||||
return lang.S.current.supplier;
|
||||
case 'Dealer':
|
||||
return lang.S.current.dealer;
|
||||
case 'Wholesaler':
|
||||
return lang.S.current.wholesaler;
|
||||
default:
|
||||
return value; // fallback
|
||||
}
|
||||
}
|
||||
|
||||
String? selectedPartyType;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
if (widget.isReport) {
|
||||
selectedPartyType = widget.type == 'supplier' ? 'Supplier' : 'All Party';
|
||||
} else {
|
||||
selectedPartyType = 'All';
|
||||
}
|
||||
}
|
||||
|
||||
List<Party> getFilteredParties(List<Party> partyList) {
|
||||
return partyList.where((c) {
|
||||
final normalizedType = (c.type ?? '').toLowerCase();
|
||||
final effectiveType = normalizedType == 'retailer' ? 'customer' : normalizedType;
|
||||
|
||||
final nameMatches = _searchText.isEmpty ||
|
||||
(c.name ?? '').toLowerCase().contains(_searchText.toLowerCase()) ||
|
||||
(c.phone ?? '').contains(_searchText);
|
||||
|
||||
// -------- REPORT MODE --------
|
||||
if (widget.isReport) {
|
||||
if (widget.type == 'supplier') {
|
||||
return effectiveType == 'supplier' && nameMatches;
|
||||
}
|
||||
|
||||
if (selectedPartyType == 'All Party') {
|
||||
return (effectiveType == 'customer' || effectiveType == 'dealer' || effectiveType == 'wholesaler') &&
|
||||
nameMatches;
|
||||
}
|
||||
|
||||
return effectiveType == selectedPartyType!.toLowerCase() && nameMatches;
|
||||
}
|
||||
|
||||
if (selectedPartyType == 'All') {
|
||||
return nameMatches;
|
||||
}
|
||||
|
||||
return effectiveType == selectedPartyType!.toLowerCase() && nameMatches;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer(
|
||||
builder: (context, ref, __) {
|
||||
final providerData = ref.watch(partiesProvider);
|
||||
final businessInfo = ref.watch(businessInfoProvider);
|
||||
final permissionService = PermissionService(ref);
|
||||
final _theme = Theme.of(context);
|
||||
final _lang = lang.S.of(context);
|
||||
|
||||
return businessInfo.when(
|
||||
data: (details) {
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
appBar: AppBar(
|
||||
backgroundColor: kWhite,
|
||||
surfaceTintColor: kWhite,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
title: Text(
|
||||
_lang.ledger,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (widget.isReport)
|
||||
businessInfo.when(
|
||||
data: (business) {
|
||||
return providerData.when(
|
||||
data: (partyList) {
|
||||
final permissionService = PermissionService(ref);
|
||||
|
||||
/// 🔹 IMPORTANT: use filtered list
|
||||
final filteredParties = getFilteredParties(partyList);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
/// ================= PDF =================
|
||||
IconButton(
|
||||
icon: HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedPdf02,
|
||||
color: kSecondayColor,
|
||||
),
|
||||
onPressed: () {
|
||||
// ---------- PERMISSION ----------
|
||||
final hasPermission = widget.type == 'supplier'
|
||||
? permissionService.hasPermission(Permit.saleReportsRead.value)
|
||||
: permissionService.hasPermission(Permit.saleReportsRead.value);
|
||||
|
||||
if (!hasPermission) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text(_lang.youDoNotHavePermissionToGenerateReport),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---------- EMPTY CHECK ----------
|
||||
if (filteredParties.isEmpty) {
|
||||
EasyLoading.showError(_lang.noDataAvailabe);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---------- GENERATE PDF ----------
|
||||
if (widget.isReport && widget.type == 'customer') {
|
||||
generateCustomerLedgerReportPdf(
|
||||
context,
|
||||
filteredParties,
|
||||
business,
|
||||
);
|
||||
} else {
|
||||
generateSupplierLedgerReportPdf(
|
||||
context,
|
||||
filteredParties,
|
||||
business,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
/// ================= EXCEL =================
|
||||
// IconButton(
|
||||
// visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
// padding: EdgeInsets.zero,
|
||||
// icon: SvgPicture.asset('assets/excel.svg'),
|
||||
// onPressed: () {
|
||||
// // ---------- PERMISSION ----------
|
||||
// if (!permissionService.hasPermission(Permit.saleReportsRead.value)) {
|
||||
// ScaffoldMessenger.of(context).showSnackBar(
|
||||
// SnackBar(
|
||||
// backgroundColor: Colors.red,
|
||||
// content: Text(_lang.youDoNotHavePermissionToExportExcel),
|
||||
// ),
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // ---------- EMPTY CHECK ----------
|
||||
// if (filteredParties.isEmpty) {
|
||||
// EasyLoading.showInfo(_lang.noDataAvailableForExport);
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // ---------- TODO: CALL EXCEL EXPORT ----------
|
||||
// // ---------- GENERATE PDF ----------
|
||||
// if (widget.isReport && widget.type == 'customer') {
|
||||
// generateCustomerLedgerReportPdf(
|
||||
// context,
|
||||
// filteredParties,
|
||||
// business,
|
||||
// );
|
||||
// } else {
|
||||
// generateSupplierLedgerReportPdf(
|
||||
// context,
|
||||
// filteredParties,
|
||||
// business,
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
);
|
||||
},
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (_, __) => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (_, __) => const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: RefreshIndicator.adaptive(
|
||||
onRefresh: () => refreshData(ref),
|
||||
child: providerData.when(
|
||||
data: (partyList) {
|
||||
if (!permissionService.hasPermission(Permit.partiesRead.value)) {
|
||||
return const Center(child: PermitDenyWidget());
|
||||
}
|
||||
// --- 1. Calculate All Summary in ONE Loop ---
|
||||
double totalCustomerDue = 0;
|
||||
double totalSupplierDue = 0;
|
||||
double summaryDue = 0;
|
||||
|
||||
for (var party in partyList) {
|
||||
final normalizedType = (party.type ?? '').toLowerCase();
|
||||
final effectiveType = normalizedType == 'retailer' ? 'customer' : normalizedType;
|
||||
|
||||
final due = party.due ?? 0;
|
||||
if (due <= 0) continue;
|
||||
|
||||
// --- TOTALS ---
|
||||
if (effectiveType == 'customer') {
|
||||
totalCustomerDue += due;
|
||||
}
|
||||
|
||||
if (effectiveType == 'supplier') {
|
||||
totalSupplierDue += due;
|
||||
}
|
||||
|
||||
// --- SUMMARY BASED ON FILTER ---
|
||||
if (widget.isReport) {
|
||||
if (selectedPartyType == 'All Party') {
|
||||
if (effectiveType == 'customer' ||
|
||||
effectiveType == 'dealer' ||
|
||||
effectiveType == 'wholesaler') {
|
||||
summaryDue += due;
|
||||
}
|
||||
} else {
|
||||
if (effectiveType == selectedPartyType!.toLowerCase()) {
|
||||
summaryDue += due;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (selectedPartyType == 'All') {
|
||||
summaryDue += due;
|
||||
} else if (effectiveType == selectedPartyType!.toLowerCase()) {
|
||||
summaryDue += due;
|
||||
}
|
||||
}
|
||||
}
|
||||
final filteredParties = getFilteredParties(partyList);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// --- SUMMARY DISPLAY ---
|
||||
if (selectedPartyType != 'All')
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 10, 16, 16),
|
||||
child: Container(
|
||||
height: 90,
|
||||
width: double.infinity,
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffFFE5F9),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text("$currency${summaryDue.toStringAsFixed(0)}",
|
||||
style: _theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
)),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"${getPartyLegerTypeLabel(selectedPartyType ?? 'All')} ${_lang.due}",
|
||||
style: _theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: kPeraColor,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// --- Summary Cards ---
|
||||
if (selectedPartyType == 'All')
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 10, 16, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
// Customer Due Card
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffFFE5F9),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text('$currency${totalCustomerDue.toStringAsFixed(2)}',
|
||||
style: _theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
)),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_lang.customerDue,
|
||||
textAlign: TextAlign.center,
|
||||
style: _theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Supplier Due Card
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF5F001A).withValues(alpha: 0.1), // Beige Light
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'$currency${totalSupplierDue.toStringAsFixed(2)}',
|
||||
style: _theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_lang.supplierDue,
|
||||
textAlign: TextAlign.center,
|
||||
style: _theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 5,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: TextFormField(
|
||||
controller: _searchController,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_searchText = value;
|
||||
});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: updateBorderColor, width: 1),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: Colors.red, width: 1),
|
||||
),
|
||||
prefixIcon: const Padding(
|
||||
padding: EdgeInsets.only(left: 10),
|
||||
child: Icon(
|
||||
FeatherIcons.search,
|
||||
color: kNeutralColor,
|
||||
),
|
||||
),
|
||||
suffixIcon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_searchController.text.isNotEmpty)
|
||||
IconButton(
|
||||
visualDensity: const VisualDensity(horizontal: -4),
|
||||
tooltip: _lang.clear,
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
setState(() {
|
||||
_searchText = '';
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
size: 20,
|
||||
color: kSubPeraColor,
|
||||
),
|
||||
),
|
||||
if (!(widget.isReport && widget.type == 'supplier'))
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
_showFilterBottomSheet(context);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(1.0),
|
||||
child: Container(
|
||||
width: 50,
|
||||
height: 45,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: kMainColor50,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(5),
|
||||
bottomRight: Radius.circular(5),
|
||||
),
|
||||
),
|
||||
child: SvgPicture.asset('assets/filter.svg'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
hintText: lang.S.of(context).searchH,
|
||||
hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: kNeutralColor,
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// --- List View ---
|
||||
Expanded(
|
||||
child: filteredParties.isEmpty
|
||||
? Center(child: EmptyWidget(message: TextSpan(text: lang.S.of(context).noParty)))
|
||||
: ListView.builder(
|
||||
itemCount: filteredParties.length,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
itemBuilder: (_, index) {
|
||||
final party = filteredParties[index];
|
||||
return _buildPartyTile(party, context, ref);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
error: (e, stack) => Text(e.toString()),
|
||||
loading: () => const Center(child: CircularProgressIndicator())),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
error: (e, stack) => Text(e.toString()),
|
||||
loading: () => const Center(child: CircularProgressIndicator()));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// --- Helper Widgets & Methods ---
|
||||
|
||||
Widget _buildPartyTile(Party party, BuildContext context, WidgetRef ref) {
|
||||
final normalizedType = (party.type ?? '').toLowerCase();
|
||||
String effectiveDisplayType;
|
||||
|
||||
if (normalizedType == 'retailer') {
|
||||
effectiveDisplayType = lang.S.of(context).customer;
|
||||
} else if (normalizedType == 'wholesaler') {
|
||||
effectiveDisplayType = lang.S.of(context).wholesaler;
|
||||
} else if (normalizedType == 'dealer') {
|
||||
effectiveDisplayType = lang.S.of(context).dealer;
|
||||
} else if (normalizedType == 'supplier') {
|
||||
effectiveDisplayType = lang.S.of(context).supplier;
|
||||
} else {
|
||||
effectiveDisplayType = normalizedType ?? '';
|
||||
}
|
||||
|
||||
// Status & Color Logic
|
||||
String statusText;
|
||||
Color statusColor;
|
||||
num? statusAmount;
|
||||
|
||||
if (party.due != null && party.due! > 0) {
|
||||
statusText = 'Due';
|
||||
statusColor = kDueColor; // Red
|
||||
statusAmount = party.due;
|
||||
} else if (party.openingBalanceType?.toLowerCase() == 'advance' && party.wallet != null && party.wallet! > 0) {
|
||||
statusText = 'Advance';
|
||||
statusColor = kAdvanceColor; // Green
|
||||
statusAmount = party.wallet;
|
||||
} else {
|
||||
statusText = lang.S.of(context).noDue;
|
||||
statusColor = kPeraColor;
|
||||
statusAmount = null;
|
||||
}
|
||||
|
||||
final _theme = Theme.of(context);
|
||||
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity(horizontal: -3, vertical: -2),
|
||||
onTap: () {
|
||||
PartyLedgerScreen(
|
||||
partyId: party.id.toString(),
|
||||
partyName: party.name.toString(),
|
||||
).launch(context);
|
||||
},
|
||||
// Avatar
|
||||
leading: CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: kMainColor50,
|
||||
backgroundImage:
|
||||
(party.image != null && party.image!.isNotEmpty) ? NetworkImage('${APIConfig.domain}${party.image}') : null,
|
||||
child: (party.image == null || party.image!.isEmpty)
|
||||
? Text(
|
||||
(party.name != null && party.name!.length >= 2)
|
||||
? party.name!.substring(0, 2)
|
||||
: (party.name != null ? party.name! : ''),
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
color: kMainColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
// Name and Type
|
||||
title: Text(
|
||||
party.name ?? '',
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
effectiveDisplayType,
|
||||
style: _theme.textTheme.bodySmall?.copyWith(
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
// Amount and Status
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (statusAmount != null)
|
||||
Text(
|
||||
'$currency${statusAmount.toStringAsFixed(0)}',
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
statusText,
|
||||
style: _theme.textTheme.bodySmall?.copyWith(
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios_rounded,
|
||||
size: 16,
|
||||
color: kPeraColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- Bottom Sheet Filter ---
|
||||
void _showFilterBottomSheet(BuildContext context) {
|
||||
String? tempSelectedType = selectedPartyType;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.white,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setModalState) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsetsGeometry.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).filter,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
style: IconButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
color: kLineColor,
|
||||
height: 1,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
DropdownButtonFormField<String>(
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: '${lang.S.of(context).partyType}*',
|
||||
),
|
||||
hint: Text(lang.S.of(context).selectOne),
|
||||
value: tempSelectedType,
|
||||
items: availablePartyTypes.map((type) {
|
||||
return DropdownMenuItem(
|
||||
value: type,
|
||||
child: Text(
|
||||
getPartyLegerTypeLabel(type),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: kTitleColor),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (widget.isReport && widget.type == 'supplier')
|
||||
? null
|
||||
: (val) {
|
||||
setModalState(() {
|
||||
tempSelectedType = val;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
setModalState(() {
|
||||
tempSelectedType = 'All';
|
||||
});
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: const BorderSide(color: Colors.red),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
lang.S.of(context).clear,
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
selectedPartyType = tempSelectedType;
|
||||
});
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFB71C1C), // Deep Red
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
lang.S.of(context).apply,
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
20
lib/Screens/party ledger/model/party_leder_filer_param.dart
Normal file
20
lib/Screens/party ledger/model/party_leder_filer_param.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
class PartyLedgerFilterParam {
|
||||
final String partyId;
|
||||
final String? duration;
|
||||
|
||||
PartyLedgerFilterParam({
|
||||
required this.partyId,
|
||||
this.duration,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is PartyLedgerFilterParam &&
|
||||
runtimeType == other.runtimeType &&
|
||||
partyId == other.partyId &&
|
||||
duration == other.duration;
|
||||
|
||||
@override
|
||||
int get hashCode => partyId.hashCode ^ duration.hashCode;
|
||||
}
|
||||
123
lib/Screens/party ledger/model/party_ledger_model.dart
Normal file
123
lib/Screens/party ledger/model/party_ledger_model.dart
Normal file
@@ -0,0 +1,123 @@
|
||||
class PartyLedgerModel {
|
||||
int? id;
|
||||
String? platform;
|
||||
// num? amount;
|
||||
num? creditAmount;
|
||||
num? debitAmount;
|
||||
String? date;
|
||||
num? balance;
|
||||
String? invoiceNumber;
|
||||
SaleModel? sale;
|
||||
PurchaseModel? purchase;
|
||||
DueModelLeger? dueCollect;
|
||||
|
||||
PartyLedgerModel({
|
||||
this.id,
|
||||
this.platform,
|
||||
this.creditAmount,
|
||||
this.debitAmount,
|
||||
this.date,
|
||||
this.balance,
|
||||
this.invoiceNumber,
|
||||
this.sale,
|
||||
this.purchase,
|
||||
this.dueCollect,
|
||||
});
|
||||
|
||||
// factory PartyLedgerModel.fromJson(Map<String, dynamic> json) {
|
||||
// // Helper to extract invoice number from nested objects
|
||||
// String? getInvoice(Map<String, dynamic> json) {
|
||||
// if (json['sale'] != null) return json['sale']['invoiceNumber'];
|
||||
// if (json['purchase'] != null) return json['purchase']['invoiceNumber'];
|
||||
// if (json['due_collect'] != null) return json['due_collect']['invoiceNumber'];
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// return PartyLedgerModel(
|
||||
// id: json['id'],
|
||||
// platform: json['platform'],
|
||||
// debitAmount: num.tryParse(json['debit_amount'].toString()),
|
||||
// creditAmount: num.tryParse(json['credit_amount'].toString()),
|
||||
// date: json['date'],
|
||||
// balance: num.tryParse(json['balance'].toString()),
|
||||
// sale: json['sale'] != null ? SaleModel.fromJson(json['sale']) : null,
|
||||
// purchase: json['purchase'] != null ? PurchaseModel.fromJson(json['purchase']) : null,
|
||||
// dueCollect: json['due_collect'] != null ? DueModelLeger.fromJson(json['due_collect']) : null,
|
||||
// invoiceNumber: getInvoice(json),
|
||||
// );
|
||||
// }
|
||||
|
||||
factory PartyLedgerModel.fromJson(Map<String, dynamic> json) {
|
||||
return PartyLedgerModel(
|
||||
id: json['id'],
|
||||
platform: json['platform'],
|
||||
debitAmount:
|
||||
json['debit_amount'] is String ? num.tryParse(json['debit_amount']) : json['debit_amount']?.toDouble(),
|
||||
creditAmount:
|
||||
json['credit_amount'] is String ? num.tryParse(json['credit_amount']) : json['credit_amount']?.toDouble(),
|
||||
date: json['date']?.toString(),
|
||||
balance: json['balance'] is String ? num.tryParse(json['balance']) : json['balance']?.toDouble(),
|
||||
invoiceNumber: json['invoice_no'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//SalePartyLegerModel
|
||||
class SaleModel {
|
||||
int? id;
|
||||
String? invoiceNumber;
|
||||
int? partyId;
|
||||
|
||||
SaleModel({this.id, this.invoiceNumber, this.partyId});
|
||||
|
||||
factory SaleModel.fromJson(Map<String, dynamic> json) {
|
||||
return SaleModel(
|
||||
id: json['id'],
|
||||
invoiceNumber: json['invoiceNumber'],
|
||||
partyId: json['party_id'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//SalePartyLegerModel
|
||||
class PurchaseModel {
|
||||
int? id;
|
||||
String? invoiceNumber;
|
||||
int? partyId;
|
||||
|
||||
PurchaseModel({this.id, this.invoiceNumber, this.partyId});
|
||||
|
||||
factory PurchaseModel.fromJson(Map<String, dynamic> json) {
|
||||
return PurchaseModel(
|
||||
id: json['id'],
|
||||
invoiceNumber: json['invoiceNumber'],
|
||||
partyId: json['party_id'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//SalePartyLegerModel
|
||||
class DueModelLeger {
|
||||
int? id;
|
||||
String? invoiceNumber;
|
||||
int? partyId;
|
||||
|
||||
DueModelLeger({this.id, this.invoiceNumber, this.partyId});
|
||||
|
||||
factory DueModelLeger.fromJson(Map<String, dynamic> json) {
|
||||
return DueModelLeger(
|
||||
id: json['id'],
|
||||
invoiceNumber: json['invoiceNumber'],
|
||||
partyId: json['party_id'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper class to return data + pagination info
|
||||
class PartyLedgerResponse {
|
||||
final List<PartyLedgerModel> data;
|
||||
final int lastPage;
|
||||
final int currentPage;
|
||||
|
||||
PartyLedgerResponse({required this.data, required this.lastPage, required this.currentPage});
|
||||
}
|
||||
189
lib/Screens/party ledger/provider.dart
Normal file
189
lib/Screens/party ledger/provider.dart
Normal file
@@ -0,0 +1,189 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Screens/party%20ledger/repo/party_ledger_repo.dart';
|
||||
import 'model/party_leder_filer_param.dart';
|
||||
import 'model/party_ledger_model.dart';
|
||||
|
||||
// State Class
|
||||
class LedgerState {
|
||||
final List<PartyLedgerModel> transactions;
|
||||
final bool isLoading;
|
||||
final bool isLoadMoreRunning;
|
||||
final int page;
|
||||
final bool hasMoreData;
|
||||
final String currentFilter;
|
||||
|
||||
LedgerState({
|
||||
this.transactions = const [],
|
||||
this.isLoading = true,
|
||||
this.isLoadMoreRunning = false,
|
||||
this.page = 1,
|
||||
this.hasMoreData = true,
|
||||
this.currentFilter = 'All',
|
||||
});
|
||||
|
||||
LedgerState copyWith({
|
||||
List<PartyLedgerModel>? transactions,
|
||||
bool? isLoading,
|
||||
bool? isLoadMoreRunning,
|
||||
int? page,
|
||||
bool? hasMoreData,
|
||||
String? currentFilter,
|
||||
}) {
|
||||
return LedgerState(
|
||||
transactions: transactions ?? this.transactions,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
isLoadMoreRunning: isLoadMoreRunning ?? this.isLoadMoreRunning,
|
||||
page: page ?? this.page,
|
||||
hasMoreData: hasMoreData ?? this.hasMoreData,
|
||||
currentFilter: currentFilter ?? this.currentFilter,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Notifier
|
||||
// class PartyLedgerNotifier extends StateNotifier<LedgerState> {
|
||||
// final PartyLedgerRepo _repository = PartyLedgerRepo();
|
||||
// final String partyId;
|
||||
//
|
||||
// PartyLedgerNotifier(this.partyId) : super(LedgerState()) {
|
||||
// loadInitialData();
|
||||
// }
|
||||
//
|
||||
// // 1. Load Initial Data (Page 1)
|
||||
// Future<void> loadInitialData() async {
|
||||
// try {
|
||||
// state = state.copyWith(isLoading: true);
|
||||
// final response = await _repository.getPartyLedger(partyId: partyId, page: 1, duration: state.currentFilter // Pass the filter
|
||||
// );
|
||||
//
|
||||
// state = state.copyWith(
|
||||
// transactions: response.data,
|
||||
// isLoading: false,
|
||||
// page: 1,
|
||||
// hasMoreData: response.currentPage < response.lastPage,
|
||||
// );
|
||||
// } catch (e) {
|
||||
// state = state.copyWith(isLoading: false, hasMoreData: false);
|
||||
// print("Error loading ledger: $e");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 2. Load More (Infinite Scroll)
|
||||
// Future<void> loadMore() async {
|
||||
// if (state.isLoadMoreRunning || !state.hasMoreData) return;
|
||||
//
|
||||
// state = state.copyWith(isLoadMoreRunning: true);
|
||||
//
|
||||
// try {
|
||||
// final nextPage = state.page + 1;
|
||||
// final response = await _repository.getPartyLedger(partyId: partyId, page: nextPage, duration: state.currentFilter // Keep using current filter
|
||||
// );
|
||||
//
|
||||
// state = state.copyWith(
|
||||
// transactions: [...state.transactions, ...response.data],
|
||||
// page: nextPage,
|
||||
// isLoadMoreRunning: false,
|
||||
// hasMoreData: response.currentPage < response.lastPage,
|
||||
// );
|
||||
// } catch (e) {
|
||||
// state = state.copyWith(isLoadMoreRunning: false);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 3. Update Filter (Resets data)
|
||||
// void updateFilter(String newFilter) {
|
||||
// if (state.currentFilter == newFilter) return;
|
||||
//
|
||||
// // Reset state but keep the new filter
|
||||
// state = LedgerState(currentFilter: newFilter, isLoading: true);
|
||||
// loadInitialData(); // Reload with new filter
|
||||
// }
|
||||
// }
|
||||
|
||||
class PartyLedgerNotifier extends StateNotifier<LedgerState> {
|
||||
final PartyLedgerRepo _repository = PartyLedgerRepo();
|
||||
final String partyId;
|
||||
|
||||
PartyLedgerNotifier({
|
||||
required this.partyId,
|
||||
required String initialFilter,
|
||||
}) : super(LedgerState(currentFilter: initialFilter)) {
|
||||
loadInitialData();
|
||||
}
|
||||
|
||||
// Load initial page (page 1)
|
||||
Future<void> loadInitialData() async {
|
||||
try {
|
||||
state = state.copyWith(isLoading: true, page: 0, hasMoreData: true);
|
||||
final response = await _repository.getPartyLedger(
|
||||
partyId: partyId,
|
||||
page: 1,
|
||||
duration: state.currentFilter,
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
transactions: response.data,
|
||||
isLoading: false,
|
||||
page: 1,
|
||||
hasMoreData: response.currentPage < response.lastPage,
|
||||
);
|
||||
} catch (e, st) {
|
||||
print('Error loading ledger: $e\n$st');
|
||||
state = state.copyWith(isLoading: false, hasMoreData: false);
|
||||
}
|
||||
}
|
||||
|
||||
// Load more for pagination
|
||||
Future<void> loadMore() async {
|
||||
if (state.isLoadMoreRunning || !state.hasMoreData) return;
|
||||
|
||||
state = state.copyWith(isLoadMoreRunning: true);
|
||||
|
||||
try {
|
||||
final nextPage = state.page + 1;
|
||||
final response = await _repository.getPartyLedger(
|
||||
partyId: partyId,
|
||||
page: nextPage,
|
||||
duration: state.currentFilter,
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
transactions: [...state.transactions, ...response.data],
|
||||
page: nextPage,
|
||||
isLoadMoreRunning: false,
|
||||
hasMoreData: response.currentPage < response.lastPage,
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error loading more ledger: $e');
|
||||
state = state.copyWith(isLoadMoreRunning: false);
|
||||
}
|
||||
}
|
||||
|
||||
// Update filter (resets data and reloads)
|
||||
void updateFilter(String newFilter) {
|
||||
if (state.currentFilter == newFilter) return;
|
||||
|
||||
state = LedgerState(
|
||||
currentFilter: newFilter,
|
||||
isLoading: true,
|
||||
transactions: [],
|
||||
page: 0,
|
||||
hasMoreData: true,
|
||||
isLoadMoreRunning: false,
|
||||
);
|
||||
|
||||
loadInitialData();
|
||||
}
|
||||
}
|
||||
|
||||
final partyLedgerProvider =
|
||||
StateNotifierProvider.family.autoDispose<PartyLedgerNotifier, LedgerState, PartyLedgerFilterParam>(
|
||||
(ref, input) => PartyLedgerNotifier(
|
||||
partyId: input.partyId,
|
||||
initialFilter: input.duration ?? '',
|
||||
),
|
||||
);
|
||||
|
||||
// final partyLedgerProvider = StateNotifierProvider.family.autoDispose<PartyLedgerNotifier, LedgerState, String>(
|
||||
// (ref, partyId) => PartyLedgerNotifier(partyId),
|
||||
// );
|
||||
152
lib/Screens/party ledger/repo/party_ledger_repo.dart
Normal file
152
lib/Screens/party ledger/repo/party_ledger_repo.dart
Normal file
@@ -0,0 +1,152 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../Const/api_config.dart';
|
||||
import '../../../http_client/customer_http_client_get.dart';
|
||||
import '../model/party_ledger_model.dart';
|
||||
|
||||
// class PartyLedgerRepo {
|
||||
// // ... existing code ...
|
||||
//
|
||||
// Future<PartyLedgerResponse> getPartyLedger({
|
||||
// required String partyId,
|
||||
// required int page,
|
||||
// String? duration, // e.g., 'today', 'this_month', etc.
|
||||
// }) async {
|
||||
// // Construct URL with pagination AND duration filter
|
||||
// String url = '${APIConfig.url}/party-ledger/$partyId?page=$page';
|
||||
//
|
||||
// // Append filter if it exists
|
||||
// if (duration != null && duration != 'All') {
|
||||
// url += '&duration=${duration.toLowerCase().replaceAll(' ', '_')}';
|
||||
// // Example: "This Month" becomes "&duration=this_month"
|
||||
// }
|
||||
//
|
||||
// final uri = Uri.parse(url);
|
||||
// CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
// final response = await clientGet.get(url: uri);
|
||||
//
|
||||
// if (response.statusCode == 200) {
|
||||
// final parsedData = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
//
|
||||
// final paginationData = parsedData['data']['data'] as List<dynamic>;
|
||||
// final metaData = parsedData['data'];
|
||||
//
|
||||
// List<PartyLedgerModel> ledgerList = paginationData.map((item) => PartyLedgerModel.fromJson(item)).toList();
|
||||
//
|
||||
// return PartyLedgerResponse(
|
||||
// data: ledgerList,
|
||||
// lastPage: metaData['last_page'] ?? 1,
|
||||
// currentPage: metaData['current_page'] ?? 1,
|
||||
// );
|
||||
// } else {
|
||||
// throw Exception('Failed to fetch ledger');
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // ... existing code ...
|
||||
// }
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
// class PartyLedgerRepo {
|
||||
// Future<PartyLedgerResponse> getPartyLedger({
|
||||
// required String partyId,
|
||||
// required int page,
|
||||
// String? duration, // same format as dashboard
|
||||
// }) async {
|
||||
// String url = '${APIConfig.url}/party-ledger/$partyId?page=$page';
|
||||
//
|
||||
// if (duration != null && duration != 'All' && duration.isNotEmpty) {
|
||||
// if (duration.startsWith('custom_date&')) {
|
||||
// final params = Uri.splitQueryString(duration.replaceFirst('custom_date&', ''));
|
||||
// final fromDate = params['from_date'];
|
||||
// final toDate = params['to_date'];
|
||||
//
|
||||
// // append exact query string as dashboard does
|
||||
// url += '&duration=custom_date&from_date=$fromDate&to_date=$toDate';
|
||||
// } else {
|
||||
// // simple durations like 'today', 'this_month', 'this_week', etc.
|
||||
// url += '&duration=$duration';
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// final uri = Uri.parse(url);
|
||||
//
|
||||
// print('-------url----${uri}-----------------');
|
||||
// final clientGet = CustomHttpClientGet(client: http.Client());
|
||||
// final response = await clientGet.get(url: uri);
|
||||
//
|
||||
// print('--------status code----${response.statusCode}-------------');
|
||||
//
|
||||
// if (response.statusCode == 200) {
|
||||
// final parsed = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
// final paginationList = parsed['data']['data'] as List<dynamic>;
|
||||
// final meta = parsed['data'];
|
||||
//
|
||||
// final ledgerList = paginationList.map((e) => PartyLedgerModel.fromJson(e as Map<String, dynamic>)).toList();
|
||||
//
|
||||
// return PartyLedgerResponse(
|
||||
// data: ledgerList,
|
||||
// lastPage: meta['last_page'] ?? 1,
|
||||
// currentPage: meta['current_page'] ?? 1,
|
||||
// );
|
||||
// } else {
|
||||
// throw Exception('Failed to fetch ledger ${response.statusCode}');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
class PartyLedgerRepo {
|
||||
Future<PartyLedgerResponse> getPartyLedger({
|
||||
required String partyId,
|
||||
required int page,
|
||||
String? duration,
|
||||
}) async {
|
||||
String url = '${APIConfig.url}/party-ledger/$partyId?page=$page';
|
||||
|
||||
if (duration != null && duration != 'All' && duration.isNotEmpty) {
|
||||
if (duration.startsWith('custom_date&')) {
|
||||
final params = Uri.splitQueryString(duration.replaceFirst('custom_date&', ''));
|
||||
final fromDate = params['from_date'];
|
||||
final toDate = params['to_date'];
|
||||
|
||||
url += '&duration=custom_date&from_date=$fromDate&to_date=$toDate';
|
||||
} else {
|
||||
url += '&duration=$duration';
|
||||
}
|
||||
}
|
||||
|
||||
final uri = Uri.parse(url);
|
||||
print('-------url----$uri-----------------');
|
||||
|
||||
final clientGet = CustomHttpClientGet(client: http.Client());
|
||||
final response = await clientGet.get(url: uri);
|
||||
|
||||
print('--------status code----${response.statusCode}-------------');
|
||||
print('--------response body----${response.body}-------------');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final parsed = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
|
||||
// FIX: Based on your JSON response, 'data' is directly an array
|
||||
// not wrapped in another 'data' object with pagination metadata
|
||||
final dataList = parsed['data'] as List<dynamic>;
|
||||
|
||||
// Since your API doesn't seem to provide pagination metadata in the response,
|
||||
// you'll need to handle pagination differently
|
||||
// For now, I'm returning default pagination values
|
||||
final ledgerList = dataList.map((e) => PartyLedgerModel.fromJson(e as Map<String, dynamic>)).toList();
|
||||
|
||||
return PartyLedgerResponse(
|
||||
data: ledgerList,
|
||||
lastPage: parsed['last_page'] ?? 1,
|
||||
currentPage: parsed['current_page'] ?? page,
|
||||
);
|
||||
} else {
|
||||
throw Exception('Failed to fetch ledger ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
}
|
||||
643
lib/Screens/party ledger/single_party_ledger_screen.dart
Normal file
643
lib/Screens/party ledger/single_party_ledger_screen.dart
Normal file
@@ -0,0 +1,643 @@
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:hugeicons/hugeicons.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Provider/profile_provider.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:mobile_pos/Provider/transactions_provider.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:mobile_pos/Screens/Due%20Calculation/Providers/due_provider.dart';
|
||||
import 'package:mobile_pos/Screens/invoice_details/due_invoice_details.dart';
|
||||
import 'package:mobile_pos/Screens/invoice_details/purchase_invoice_details.dart';
|
||||
import 'package:mobile_pos/Screens/invoice_details/sales_invoice_details_screen.dart';
|
||||
import 'package:mobile_pos/Screens/party%20ledger/provider.dart';
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
import 'package:mobile_pos/pdf_report/ledger_report/ledger_report_pdf.dart';
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../constant.dart';
|
||||
import '../../model/business_info_model.dart';
|
||||
import '../../pdf_report/ledger_report/ledger_report_excel.dart';
|
||||
import '../../widgets/build_date_selector/build_date_selector.dart';
|
||||
import '../Sales/Repo/sales_repo.dart';
|
||||
import 'model/party_leder_filer_param.dart';
|
||||
|
||||
class PartyLedgerScreen extends ConsumerStatefulWidget {
|
||||
final String partyId;
|
||||
final String partyName; // Passed for the Appbar title
|
||||
|
||||
const PartyLedgerScreen({
|
||||
super.key,
|
||||
required this.partyId,
|
||||
required this.partyName,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<PartyLedgerScreen> createState() => _PartyLedgerScreenState();
|
||||
}
|
||||
|
||||
class _PartyLedgerScreenState extends ConsumerState<PartyLedgerScreen> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
final Map<String, String> dateOptions = {
|
||||
'all': lang.S.current.all,
|
||||
'today': lang.S.current.today,
|
||||
'yesterday': lang.S.current.yesterday,
|
||||
'last_seven_days': lang.S.current.last7Days,
|
||||
'last_thirty_days': lang.S.current.last30Days,
|
||||
'current_month': lang.S.current.currentMonth,
|
||||
'last_month': lang.S.current.lastMonth,
|
||||
'current_year': lang.S.current.currentYear,
|
||||
'custom_date': lang.S.current.customerDate,
|
||||
};
|
||||
String selectedTime = 'all';
|
||||
// String selectedTime = 'today';
|
||||
bool _isRefreshing = false; // Prevents multiple refresh calls
|
||||
|
||||
Future<void> refreshData(WidgetRef ref) async {
|
||||
if (_isRefreshing) return; // Prevent duplicate refresh calls
|
||||
_isRefreshing = true;
|
||||
|
||||
ref.refresh(dashboardInfoProvider(selectedTime.toLowerCase()));
|
||||
|
||||
await Future.delayed(const Duration(seconds: 1)); // Optional delay
|
||||
_isRefreshing = false;
|
||||
}
|
||||
|
||||
bool _showCustomDatePickers = false; // Track if custom date pickers should be shown
|
||||
|
||||
DateTime? fromDate;
|
||||
DateTime? toDate;
|
||||
|
||||
String? _getDateRangeString() {
|
||||
if (selectedTime == 'all') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (selectedTime != 'custom_date') {
|
||||
return selectedTime.toLowerCase();
|
||||
}
|
||||
|
||||
if (fromDate != null && toDate != null) {
|
||||
final formattedFrom = DateFormat('yyyy-MM-dd').format(fromDate!);
|
||||
final formattedTo = DateFormat('yyyy-MM-dd').format(toDate!);
|
||||
return 'custom_date&from_date=$formattedFrom&to_date=$formattedTo';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _selectedFormDate(BuildContext context) async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
firstDate: DateTime(2021),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
if (picked != null && picked != fromDate) {
|
||||
setState(() {
|
||||
fromDate = picked;
|
||||
});
|
||||
if (toDate != null) refreshData(ref);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _selectToDate(BuildContext context) async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
firstDate: fromDate ?? DateTime(2021),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
if (picked != null && picked != toDate) {
|
||||
setState(() {
|
||||
toDate = picked;
|
||||
});
|
||||
if (fromDate != null) refreshData(ref);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to format date "27 Jan 2025"
|
||||
String _formatDate(String? dateStr) {
|
||||
if (dateStr == null) return '-';
|
||||
try {
|
||||
DateTime date = DateTime.parse(dateStr);
|
||||
return DateFormat('dd MMM yyyy').format(date);
|
||||
} catch (e) {
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final dateRangeString = _getDateRangeString();
|
||||
final filterParam = PartyLedgerFilterParam(
|
||||
partyId: widget.partyId,
|
||||
duration: dateRangeString,
|
||||
);
|
||||
|
||||
_scrollController.addListener(() {
|
||||
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 100) {
|
||||
ref.read(partyLedgerProvider(filterParam).notifier).loadMore();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _lang = lang.S.of(context);
|
||||
final dateRangeString = _getDateRangeString();
|
||||
print('-----------party id-----------${widget.partyId}---filter: $dateRangeString');
|
||||
final filterParam = PartyLedgerFilterParam(
|
||||
partyId: widget.partyId,
|
||||
duration: dateRangeString,
|
||||
);
|
||||
|
||||
final ledgerState = ref.watch(partyLedgerProvider(filterParam));
|
||||
|
||||
final notifier = ref.read(partyLedgerProvider(filterParam).notifier);
|
||||
final businessData = ref.watch(businessInfoProvider);
|
||||
final saleTransactionData = ref.watch(salesTransactionProvider);
|
||||
final purchaseTransactionData = ref.watch(purchaseTransactionProvider);
|
||||
final dueTransactionData = ref.watch(dueCollectionListProvider);
|
||||
final _theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leadingWidth: 30,
|
||||
title: Text(
|
||||
widget.partyName,
|
||||
style: _theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
businessData.when(
|
||||
data: (business) {
|
||||
return IconButton(
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
if (ledgerState.transactions.isNotEmpty) {
|
||||
generateLedgerReportPdf(
|
||||
context,
|
||||
ledgerState.transactions,
|
||||
business,
|
||||
_showCustomDatePickers ? fromDate : null,
|
||||
_showCustomDatePickers ? toDate : null,
|
||||
selectedTime,
|
||||
);
|
||||
} else {
|
||||
EasyLoading.showInfo(_lang.noTransactionToGeneratePdf);
|
||||
}
|
||||
},
|
||||
icon: HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedPdf01,
|
||||
color: kSecondayColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
error: (e, stack) => Center(child: Text(e.toString())),
|
||||
loading: () => Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
businessData.when(
|
||||
data: (business) {
|
||||
return IconButton(
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
if (ledgerState.transactions.isNotEmpty) {
|
||||
generateLedgerReportExcel(
|
||||
context,
|
||||
ledgerState.transactions,
|
||||
business,
|
||||
_showCustomDatePickers ? fromDate : null,
|
||||
_showCustomDatePickers ? toDate : null,
|
||||
selectedTime,
|
||||
);
|
||||
} else {
|
||||
EasyLoading.showInfo(_lang.generatePdf);
|
||||
}
|
||||
},
|
||||
icon: SvgPicture.asset('assets/excel.svg'),
|
||||
);
|
||||
},
|
||||
error: (e, stack) => Center(child: Text(e.toString())),
|
||||
loading: () => Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// --- Filter Dropdown ---
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: SizedBox(
|
||||
width: 120,
|
||||
height: 32,
|
||||
child: DropdownButtonFormField2<String>(
|
||||
isExpanded: true,
|
||||
iconStyleData: IconStyleData(
|
||||
icon: Icon(Icons.keyboard_arrow_down, color: kPeraColor, size: 20),
|
||||
),
|
||||
value: selectedTime,
|
||||
items: dateOptions.entries.map((entry) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: entry.key,
|
||||
child: Text(
|
||||
entry.value,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _theme.textTheme.titleSmall?.copyWith(
|
||||
color: kPeraColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedTime = value!;
|
||||
_showCustomDatePickers = selectedTime == 'custom_date';
|
||||
|
||||
if (_showCustomDatePickers) {
|
||||
fromDate = DateTime.now().subtract(const Duration(days: 7));
|
||||
toDate = DateTime.now();
|
||||
}
|
||||
|
||||
if (selectedTime != 'custom_date') {
|
||||
refreshData(ref);
|
||||
}
|
||||
});
|
||||
},
|
||||
dropdownStyleData: DropdownStyleData(
|
||||
maxHeight: 500,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
scrollbarTheme: ScrollbarThemeData(
|
||||
radius: const Radius.circular(40),
|
||||
thickness: WidgetStateProperty.all<double>(6),
|
||||
thumbVisibility: WidgetStateProperty.all<bool>(true),
|
||||
),
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(padding: EdgeInsets.symmetric(horizontal: 6)),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
bottom: _showCustomDatePickers
|
||||
? PreferredSize(
|
||||
preferredSize: const Size.fromHeight(50),
|
||||
child: Column(
|
||||
children: [
|
||||
Divider(thickness: 1, color: kBottomBorder, height: 1),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => _selectedFormDate(context),
|
||||
child: buildDateSelector(
|
||||
prefix: 'From',
|
||||
date: fromDate != null ? DateFormat('dd MMMM yyyy').format(fromDate!) : 'Select Date',
|
||||
theme: _theme,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
RotatedBox(
|
||||
quarterTurns: 1,
|
||||
child: Container(
|
||||
height: 1,
|
||||
width: 22,
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
GestureDetector(
|
||||
onTap: () => _selectToDate(context),
|
||||
child: buildDateSelector(
|
||||
prefix: 'To',
|
||||
date: toDate != null ? DateFormat('dd MMMM yyyy').format(toDate!) : 'Select Date',
|
||||
theme: _theme,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async => notifier.updateFilter(ledgerState.currentFilter),
|
||||
child: ledgerState.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ledgerState.transactions.isEmpty
|
||||
? Center(child: Text(_lang.noTransactionFound))
|
||||
: RefreshIndicator(
|
||||
onRefresh: () async => notifier.updateFilter(ledgerState.currentFilter),
|
||||
child: SingleChildScrollView(
|
||||
controller: _scrollController, // keep infinite scroll
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
scrollDirection: Axis.vertical,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal, // horizontal scroll
|
||||
child: DataTable(
|
||||
headingRowColor: WidgetStateProperty.all(
|
||||
Color(0xffF5F3F3).withValues(alpha: 0.5),
|
||||
),
|
||||
dividerThickness: 1,
|
||||
// --- Header -------------
|
||||
columns: [
|
||||
DataColumn(
|
||||
label: Text(
|
||||
_lang.date,
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
_lang.reference,
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
_lang.description,
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
_lang.creditIn,
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
_lang.debitOut,
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
_lang.balance,
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// --- Rows (converted from ListView rows) --- //
|
||||
rows: [
|
||||
...ledgerState.transactions.map((data) {
|
||||
return DataRow(
|
||||
cells: [
|
||||
// 1. Date
|
||||
DataCell(
|
||||
Text(
|
||||
_formatDate(data.date),
|
||||
style: _theme.textTheme.bodyLarge?.copyWith(
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
DataCell(
|
||||
saleTransactionData.when(
|
||||
data: (sales) {
|
||||
return purchaseTransactionData.when(
|
||||
data: (purchases) {
|
||||
return dueTransactionData.when(
|
||||
data: (dueCollections) {
|
||||
return businessData.when(
|
||||
data: (business) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if (data.platform == 'Sales') {
|
||||
final sale = sales.firstWhereOrNull((e) => e.id == data.id);
|
||||
if (sale != null) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => SalesInvoiceDetails(
|
||||
saleTransaction: sale,
|
||||
businessInfo: business,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Sale not found')),
|
||||
);
|
||||
}
|
||||
} else if (data.platform == 'Purchase') {
|
||||
final purchase =
|
||||
purchases.firstWhereOrNull((e) => e.id == data.id);
|
||||
if (purchase != null) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => PurchaseInvoiceDetails(
|
||||
transitionModel: purchase,
|
||||
businessInfo: business,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Purchase not found')),
|
||||
);
|
||||
}
|
||||
} else if (data.platform == 'Payment') {
|
||||
final due =
|
||||
dueCollections.firstWhereOrNull((e) => e.id == data.id);
|
||||
if (due != null) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => DueInvoiceDetails(
|
||||
dueCollection: due,
|
||||
personalInformationModel: business,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Due Collection not found')),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
data.invoiceNumber ?? '-',
|
||||
textAlign: TextAlign.center,
|
||||
style: _theme.textTheme.bodyLarge?.copyWith(
|
||||
color: Colors.red,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, stack) => Center(child: Text(e.toString())),
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, stack) => Center(child: Text(e.toString())),
|
||||
);
|
||||
},
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (e, stack) => Center(child: Text(e.toString())),
|
||||
);
|
||||
},
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (e, stack) => Center(child: Text(e.toString())),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
data.platform.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: _theme.textTheme.bodyLarge?.copyWith(
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Credit
|
||||
DataCell(
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'$currency${formatPointNumber(data.creditAmount ?? 0, addComma: true)}',
|
||||
style: _theme.textTheme.bodyLarge?.copyWith(
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Debit
|
||||
DataCell(
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'$currency${formatPointNumber(data.debitAmount ?? 0, addComma: true)}',
|
||||
style: _theme.textTheme.bodyLarge?.copyWith(
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Balance
|
||||
DataCell(
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'$currency${formatPointNumber(data.balance ?? 0, addComma: true)}',
|
||||
style: _theme.textTheme.bodyLarge?.copyWith(
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
|
||||
// --- Load More Loader Row --- //
|
||||
if (ledgerState.isLoadMoreRunning)
|
||||
const DataRow(
|
||||
cells: [
|
||||
DataCell(
|
||||
Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
),
|
||||
DataCell(Text("")),
|
||||
DataCell(Text("")),
|
||||
DataCell(Text("")),
|
||||
DataCell(Text("")),
|
||||
DataCell(Text("")),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffF5F3F3).withValues(alpha: 0.5),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_lang.totalBalance,
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
ledgerState.transactions.isNotEmpty
|
||||
? "$currency${formatPointNumber(ledgerState.transactions.last.balance ?? 0)}"
|
||||
: "0",
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user