first commit

This commit is contained in:
2026-02-07 15:57:09 +07:00
commit 157096f164
1153 changed files with 415766 additions and 0 deletions

View 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),
),
),
),
],
),
],
),
),
],
);
},
);
},
);
}
}

View 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;
}

View 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});
}

View 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),
// );

View 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}');
}
}
}

View 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,
),
),
],
),
),
);
}
}