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,284 @@
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:hugeicons/hugeicons.dart';
import 'package:mobile_pos/generated/l10n.dart' as l;
import '../../../GlobalComponents/glonal_popup.dart';
import '../../../Provider/profile_provider.dart';
import '../../../constant.dart';
import '../../../currency.dart';
import '../../../pdf_report/ledger_report_pdf/customer_ledger_report_pdf.dart';
import '../../../service/check_user_role_permission_provider.dart';
import '../../../widgets/empty_widget/_empty_widget.dart';
import '../../Customers/Provider/customer_provider.dart';
class CustomerLedgerReport extends ConsumerStatefulWidget {
const CustomerLedgerReport({super.key});
@override
ConsumerState<CustomerLedgerReport> createState() => _CustomerLedgerReportState();
}
class _CustomerLedgerReportState extends ConsumerState<CustomerLedgerReport> {
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;
}
@override
Widget build(BuildContext context) {
return Consumer(
builder: (context, ref, __) {
final providerData = ref.watch(partiesProvider);
final businessInfo = ref.watch(businessInfoProvider);
final personalData = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
final _theme = Theme.of(context);
final _lang = l.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.customerLedger,
),
actions: [
personalData.when(
data: (business) {
return providerData.when(
data: (transaction) {
return Row(
children: [
IconButton(
onPressed: () {
if (transaction.isNotEmpty) {
generateCustomerLedgerReportPdf(context, transaction, business);
} else {
EasyLoading.showError(_lang.listIsEmpty);
}
},
icon: HugeIcon(icon: HugeIcons.strokeRoundedPdf02, color: kSecondayColor),
),
/*
IconButton(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
onPressed: () {
if (!permissionService.hasPermission(Permit.expenseReportsRead.value)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text('You do not have permission to view expense report.'),
),
);
return;
}
if (transaction.isNotEmpty) {
} else {
EasyLoading.showInfo('No data available for generate pdf');
}
},
icon: SvgPicture.asset('assets/excel.svg'),
),
*/
SizedBox(width: 8),
],
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: SizedBox.shrink,
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: SizedBox.shrink,
),
],
bottom: PreferredSize(
preferredSize: Size(double.infinity, 70),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SizedBox(
height: 45,
child: TextFormField(
controller: _searchController,
onChanged: (value) {
setState(() {
_searchText = value;
});
},
decoration: InputDecoration(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
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: _searchController.text.isNotEmpty
? IconButton(
onPressed: () {
_searchController.clear();
setState(() {
_searchText = '';
});
},
icon: Icon(
Icons.close,
size: 20,
color: kSubPeraColor,
),
)
: null,
hintText: l.S.of(context).searchH,
hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: kNeutralColor,
),
),
),
),
),
SizedBox(height: 8),
Divider(color: kBottomBorder),
],
),
),
),
body: RefreshIndicator.adaptive(
onRefresh: () => refreshData(ref),
child: providerData.when(
data: (partyList) {
if (!permissionService.hasPermission(Permit.partiesRead.value)) {
return const Center(child: PermitDenyWidget());
}
// --- Filter to only Customer, Dealer, Wholesaler ---
final filteredParties = partyList.where((party) {
final type = (party.type ?? '').toLowerCase();
final nameMatches = _searchText.isEmpty
? true
: (party.name ?? '').toLowerCase().contains(_searchText.toLowerCase()) ||
(party.phone ?? '').contains(_searchText);
final showType =
type == 'customer' || type == 'dealer' || type == 'wholesaler' || type == 'retailer';
return showType && nameMatches;
}).toList();
return filteredParties.isEmpty
? Center(child: EmptyWidget(message: TextSpan(text: l.S.of(context).noParty)))
: ListView.separated(
itemCount: filteredParties.length,
padding: EdgeInsets.zero,
itemBuilder: (_, index) {
final party = filteredParties[index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: GestureDetector(
// onTap: () {
// PartyLedgerScreen(
// partyId: party.id.toString(),
// partyName: party.name.toString(),
// ).launch(context);
// },
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
party.name ?? '',
style: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
Text(
'${_lang.due}: $currency${formatPointNumber(party.due ?? 0, addComma: true)}',
style: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
],
),
SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${_lang.type}: ${party.type ?? ''}',
style: _theme.textTheme.bodyMedium,
),
Text(
'${_lang.amount}: ${party.totalSaleAmount ?? 0}',
style: _theme.textTheme.bodyMedium,
),
],
),
SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${_lang.totalSales} : ${party.saleCount}',
style: _theme.textTheme.bodyMedium,
),
Text(
'${_lang.paidAmount} : $currency${formatPointNumber(party.totalSalePaid ?? 0, addComma: true)}',
style: _theme.textTheme.bodyMedium,
),
],
),
],
),
),
);
},
separatorBuilder: (_, __) => Divider(
thickness: 1,
height: 1,
color: kBottomBorder,
),
);
},
error: (e, stack) => Text(e.toString()),
loading: () => const Center(child: CircularProgressIndicator()),
),
),
),
);
},
error: (e, stack) => Text(e.toString()),
loading: () => const Center(child: CircularProgressIndicator()),
);
},
);
}
}

View File

@@ -0,0 +1,209 @@
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_pos/currency.dart';
import 'package:mobile_pos/generated/l10n.dart' as l;
import '../../../GlobalComponents/glonal_popup.dart';
import '../../../constant.dart';
import '../../../service/check_user_role_permission_provider.dart';
import '../../../widgets/empty_widget/_empty_widget.dart';
import '../../Customers/Model/parties_model.dart';
import '../../Customers/Provider/customer_provider.dart';
class PartyWiseLossProfitDetails extends ConsumerStatefulWidget {
final Party party;
const PartyWiseLossProfitDetails({super.key, required this.party});
@override
ConsumerState<PartyWiseLossProfitDetails> createState() => _PartyWiseLossProfitDetailsState();
}
class _PartyWiseLossProfitDetailsState extends ConsumerState<PartyWiseLossProfitDetails> {
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;
}
@override
Widget build(BuildContext context) {
final permissionService = PermissionService(ref);
final _theme = Theme.of(context);
final _lang = l.S.of(context);
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.details,
),
bottom: PreferredSize(
preferredSize: const Size(double.infinity, 70),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: SizedBox(
height: 45,
child: TextFormField(
controller: _searchController,
onChanged: (value) {
setState(() {
_searchText = value;
});
},
decoration: InputDecoration(
contentPadding: EdgeInsets.zero,
//
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: _searchController.text.isNotEmpty
? IconButton(
onPressed: () {
_searchController.clear();
setState(() {
_searchText = '';
});
},
icon: Icon(
Icons.close,
size: 20,
color: kSubPeraColor,
),
)
: null,
hintText: l.S.of(context).searchH,
hintStyle: _theme.textTheme.bodyMedium?.copyWith(color: kNeutralColor),
),
),
),
),
),
),
body: RefreshIndicator.adaptive(
onRefresh: () => refreshData(ref),
child: _buildTransactionList(context),
),
),
);
}
Widget _buildTransactionList(BuildContext context) {
final _theme = Theme.of(context);
final _lang = l.S.of(context);
final permissionService = PermissionService(ref);
if (!permissionService.hasPermission(Permit.partiesRead.value)) {
return const Center(child: PermitDenyWidget());
}
final transactions = widget.party.sales ?? [];
final filteredTransactions = transactions
.where((tx) => _searchText.isEmpty
? true
: (tx.salesDetails ?? [])
.any((d) => (d.product?.productName ?? '').toLowerCase().contains(_searchText.toLowerCase())))
.toList();
if (filteredTransactions.isEmpty) {
return Center(child: EmptyWidget(message: TextSpan(text: l.S.of(context).noProductFound)));
}
return ListView.separated(
padding: EdgeInsets.zero,
itemCount: filteredTransactions.length,
itemBuilder: (_, index) {
final tx = filteredTransactions[index];
final details = tx.salesDetails ?? [];
return Column(
children: details
.where((d) => _searchText.isEmpty
? true
: (d.product?.productName ?? '').toLowerCase().contains(_searchText.toLowerCase()))
.map(
(d) {
final profitLoss = d.lossProfit ?? 0;
final profit = profitLoss > 0 ? profitLoss : 0;
final loss = profitLoss < 0 ? profitLoss.abs() : 0;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
d.product?.productName ?? '',
style: _theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500),
),
Text(
'${l.S.of(context).qty}: ${d.quantities ?? 0}',
style: _theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500),
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${_lang.purchasePrice}: $currency${formatPointNumber(d.productPurchasePrice ?? 0, addComma: true)}',
style: _theme.textTheme.bodyMedium,
),
Text(
'${_lang.salePrice}: $currency${formatPointNumber(d.price ?? 0, addComma: true)}',
style: _theme.textTheme.bodyMedium,
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${_lang.loss}: $currency${formatPointNumber(loss, addComma: true)}',
style: _theme.textTheme.bodyMedium?.copyWith(color: Colors.red),
),
Text(
'${_lang.profit}: $currency${formatPointNumber(profit, addComma: true)}',
style: _theme.textTheme.bodyMedium?.copyWith(color: Colors.green),
),
],
),
],
),
);
},
).toList(),
);
},
separatorBuilder: (_, __) => const Divider(thickness: 1, height: 1, color: kLineColor),
);
}
}

View File

@@ -0,0 +1,303 @@
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:hugeicons/hugeicons.dart';
import 'package:mobile_pos/Screens/Report/party_report/party_wise_loss_profit_details.dart';
import 'package:mobile_pos/core/theme/_app_colors.dart';
import 'package:mobile_pos/generated/l10n.dart' as l;
import '../../../GlobalComponents/glonal_popup.dart';
import '../../../Provider/profile_provider.dart';
import '../../../constant.dart';
import '../../../currency.dart';
import '../../../pdf_report/party/party_wise_loss_profit_report_pdf.dart';
import '../../../service/check_user_role_permission_provider.dart';
import '../../../widgets/empty_widget/_empty_widget.dart';
import '../../Customers/Provider/customer_provider.dart';
class PartyWiseProfitAndLoss extends ConsumerStatefulWidget {
const PartyWiseProfitAndLoss({super.key});
@override
ConsumerState<PartyWiseProfitAndLoss> createState() => _CustomerLedgerReportState();
}
class _CustomerLedgerReportState extends ConsumerState<PartyWiseProfitAndLoss> {
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;
}
@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 = l.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.partyWiseProfit,
),
bottom: PreferredSize(
preferredSize: Size(double.infinity, 70),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SizedBox(
height: 45,
child: TextFormField(
controller: _searchController,
onChanged: (value) {
setState(() {
_searchText = value;
});
},
decoration: InputDecoration(
contentPadding: EdgeInsets.zero,
//
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: _searchController.text.isNotEmpty
? IconButton(
onPressed: () {
_searchController.clear();
setState(() {
_searchText = '';
});
},
icon: Icon(
Icons.close,
size: 20,
color: kSubPeraColor,
),
)
: null,
hintText: l.S.of(context).searchH,
hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: kNeutralColor,
),
),
),
),
),
SizedBox(height: 8),
Divider(color: kBottomBorder),
],
),
),
actions: [
businessInfo.when(
data: (business) {
return providerData.when(
data: (partyList) {
final filteredParties = partyList.where((party) {
final type = (party.type ?? '').toLowerCase();
final isValidType =
type == 'customer' || type == 'dealer' || type == 'wholesaler' || type == 'retailer';
if (!isValidType) return false;
if (_searchText.isEmpty) return true;
final query = _searchText.toLowerCase();
return (party.name ?? '').toLowerCase().contains(query) ||
(party.phone ?? '').contains(query);
}).toList();
return Row(
children: [
/// PDF
IconButton(
icon: HugeIcon(icon: HugeIcons.strokeRoundedPdf02, color: kSecondayColor),
onPressed: () {
if (filteredParties.isEmpty) {
EasyLoading.showError(_lang.noDataAvailable);
return;
}
generatePartyWiseLossProfitReportPdf(context, filteredParties, business);
},
),
/// EXCEL
/*
IconButton(
icon: SvgPicture.asset('assets/excel.svg'),
onPressed: () {
if (filteredParties.isEmpty) {
EasyLoading.showInfo('No data available for export');
return;
}
// exportLedgerExcel(filteredParties, businessInfoData);
},
),
*/
],
);
},
loading: () => const SizedBox.shrink(),
error: (_, __) => const SizedBox.shrink(),
);
},
error: (e, stack) => Center(
child: Text(e.toString()),
),
loading: SizedBox.shrink,
),
],
),
body: RefreshIndicator.adaptive(
onRefresh: () => refreshData(ref),
child: providerData.when(
data: (partyList) {
if (!permissionService.hasPermission(Permit.partiesRead.value)) {
return const Center(child: PermitDenyWidget());
}
final filteredParties = partyList.where((party) {
final type = (party.type ?? '').toLowerCase();
final isValidType =
type == 'customer' || type == 'dealer' || type == 'wholesaler' || type == 'retailer';
if (!isValidType) return false;
// Apply search filter
if (_searchText.isEmpty) return true;
final query = _searchText.toLowerCase();
return (party.name ?? '').toLowerCase().contains(query) || (party.phone ?? '').contains(query);
}).toList();
return filteredParties.isEmpty
? Center(child: EmptyWidget(message: TextSpan(text: l.S.of(context).noParty)))
: ListView.separated(
itemCount: filteredParties.length,
padding: EdgeInsets.zero,
itemBuilder: (_, index) {
final party = filteredParties[index];
// final num profitLoss = party.totalSaleLossProfit ?? 0;
//
// final num profitAmount = profitLoss > 0 ? profitLoss : 0;
// final num lossAmount = profitLoss < 0 ? profitLoss.abs() : 0;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: GestureDetector(
onTap: () {
if (party.sales == null || party.sales!.isEmpty) {
EasyLoading.showError(_lang.noDataFound);
return;
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PartyWiseLossProfitDetails(party: party)));
}
},
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
party.name ?? '',
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
Text.rich(
TextSpan(
text: '${_lang.profit}: ',
children: [
TextSpan(
text:
'$currency${formatPointNumber(party.totalSaleProfit ?? 0, addComma: true)}',
style: _theme.textTheme.titleMedium?.copyWith(
color: DAppColors.kSuccess,
)),
],
style: _theme.textTheme.bodyLarge,
),
),
],
),
SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${_lang.sale}: ${formatPointNumber(party.totalSaleAmount ?? 0, addComma: true)}',
style: _theme.textTheme.bodyLarge,
),
Text.rich(
TextSpan(
text: '${_lang.loss}: ',
children: [
TextSpan(
text:
'$currency${formatPointNumber(party.totalSaleLoss?.abs() ?? 0, addComma: true)}',
style: TextStyle(color: DAppColors.kError)),
],
style: _theme.textTheme.bodyLarge,
),
),
],
),
],
),
),
);
},
separatorBuilder: (_, __) => Divider(
thickness: 1,
height: 1,
color: kBottomBorder,
),
);
},
error: (e, stack) => Text(e.toString()),
loading: () => const Center(child: CircularProgressIndicator()),
),
),
),
);
},
error: (e, stack) => Text(e.toString()),
loading: () => const Center(child: CircularProgressIndicator()),
);
},
);
}
}

View File

@@ -0,0 +1,295 @@
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:hugeicons/hugeicons.dart';
import 'package:mobile_pos/currency.dart';
import 'package:mobile_pos/generated/l10n.dart' as l;
import '../../../GlobalComponents/glonal_popup.dart';
import '../../../Provider/profile_provider.dart';
import '../../../constant.dart';
import '../../../pdf_report/ledger_report_pdf/supplier_ledger_report_pdf.dart';
import '../../../service/check_user_role_permission_provider.dart';
import '../../../widgets/empty_widget/_empty_widget.dart';
import '../../Customers/Provider/customer_provider.dart';
class SupplierLedger extends ConsumerStatefulWidget {
const SupplierLedger({super.key});
@override
ConsumerState<SupplierLedger> createState() => _CustomerLedgerReportState();
}
class _CustomerLedgerReportState extends ConsumerState<SupplierLedger> {
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;
}
@override
Widget build(BuildContext context) {
return Consumer(
builder: (context, ref, __) {
final providerData = ref.watch(partiesProvider);
final businessInfo = ref.watch(businessInfoProvider);
final personalData = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
final _theme = Theme.of(context);
final _lang = l.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.supplierLedger,
),
bottom: PreferredSize(
preferredSize: Size(double.infinity, 70),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SizedBox(
height: 45,
child: TextFormField(
controller: _searchController,
onChanged: (value) {
setState(() {
_searchText = value;
});
},
decoration: InputDecoration(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
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: _searchController.text.isNotEmpty
? IconButton(
onPressed: () {
_searchController.clear();
setState(() {
_searchText = '';
});
},
icon: Icon(
Icons.close,
size: 20,
color: kSubPeraColor,
),
)
: null,
hintText: l.S.of(context).searchH,
hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: kNeutralColor,
),
),
),
),
),
SizedBox(height: 8),
Divider(color: kBottomBorder),
],
),
),
actions: [
personalData.when(
data: (business) {
return providerData.when(
data: (transaction) {
return Row(
children: [
IconButton(
onPressed: () {
if (transaction.isNotEmpty) {
generateSupplierLedgerReportPdf(context, transaction, business);
} else {
EasyLoading.showError(_lang.listIsEmpty);
}
},
icon: HugeIcon(icon: HugeIcons.strokeRoundedPdf02, color: kSecondayColor),
),
/*
IconButton(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
onPressed: () {
if (!permissionService.hasPermission(Permit.expenseReportsRead.value)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text('You do not have permission to view expense report.'),
),
);
return;
}
if (transaction.isNotEmpty) {
} else {
EasyLoading.showInfo('No data available for generate pdf');
}
},
icon: SvgPicture.asset('assets/excel.svg'),
),
*/
SizedBox(width: 8),
],
);
},
error: (e, stack) => Center(
child: Text(e.toString()),
),
loading: SizedBox.shrink,
);
},
error: (e, stack) => Center(
child: Text(e.toString()),
),
loading: SizedBox.shrink,
)
],
),
body: RefreshIndicator.adaptive(
onRefresh: () => refreshData(ref),
child: providerData.when(
data: (partyList) {
if (!permissionService.hasPermission(Permit.partiesRead.value)) {
return const Center(child: PermitDenyWidget());
}
// --- Filter to only Customer, Dealer, Wholesaler ---
final filteredParties = partyList.where((party) {
final type = (party.type ?? '').toLowerCase();
final nameMatches = _searchText.isEmpty
? true
: (party.name ?? '').toLowerCase().contains(_searchText.toLowerCase()) ||
(party.phone ?? '').contains(_searchText);
final showType = type == 'supplier';
return showType && nameMatches;
}).toList();
// --- Calculate Total Due ---
double totalDue = 0;
for (var party in filteredParties) {
if (party.due != null && party.due! > 0) {
totalDue += party.due!;
}
}
return filteredParties.isEmpty
? Center(child: EmptyWidget(message: TextSpan(text: l.S.of(context).noParty)))
: ListView.separated(
itemCount: filteredParties.length,
padding: EdgeInsets.zero,
itemBuilder: (_, index) {
final party = filteredParties[index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: GestureDetector(
// onTap: () {
// PartyLedgerScreen(
// partyId: party.id.toString(),
// partyName: party.name.toString(),
// ).launch(context);
// },
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
party.name ?? '',
style: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
Text(
'${_lang.due}: $currency${formatPointNumber(party.due ?? 0, addComma: true)}',
style: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
],
),
SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${_lang.type}: ${party.type ?? ''}',
style: _theme.textTheme.bodyMedium,
),
Text(
'${_lang.amount}: ${party.totalPurchaseAmount}',
style: _theme.textTheme.bodyMedium,
),
],
),
SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${_lang.totalPurchase} : ${party.purchaseCount ?? 0}',
style: _theme.textTheme.bodyMedium,
),
Text(
'${_lang.paidAmount} : $currency${formatPointNumber(party.totalPurchasePaid ?? 0, addComma: true)}',
style: _theme.textTheme.bodyMedium,
),
],
),
],
),
),
);
},
separatorBuilder: (_, __) => Divider(
thickness: 1,
height: 1,
color: kBottomBorder,
),
);
},
error: (e, stack) => Text(e.toString()),
loading: () => const Center(child: CircularProgressIndicator()),
),
),
),
);
},
error: (e, stack) => Text(e.toString()),
loading: () => const Center(child: CircularProgressIndicator()),
);
},
);
}
}

View File

@@ -0,0 +1,211 @@
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hugeicons/hugeicons.dart';
import 'package:mobile_pos/generated/l10n.dart' as l;
import '../../../GlobalComponents/glonal_popup.dart';
import '../../../Provider/profile_provider.dart';
import '../../../constant.dart';
import '../../Customers/Model/parties_model.dart';
import '../../../pdf_report/party/top_5_customer_report_pdf.dart';
import '../../../service/check_user_role_permission_provider.dart';
import '../../../widgets/empty_widget/_empty_widget.dart';
import '../../Customers/Provider/customer_provider.dart';
class TopFiveCustomer extends ConsumerStatefulWidget {
const TopFiveCustomer({super.key});
@override
ConsumerState<TopFiveCustomer> createState() => _CustomerLedgerReportState();
}
class _CustomerLedgerReportState extends ConsumerState<TopFiveCustomer> {
bool _isRefreshing = false;
final searchController = TextEditingController();
Future<void> refreshData(WidgetRef ref) async {
if (_isRefreshing) return;
_isRefreshing = true;
ref.refresh(partiesProvider);
await Future.delayed(const Duration(seconds: 1));
_isRefreshing = false;
}
@override
Widget build(BuildContext context) {
final _lang = l.S.of(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);
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.top5Customer,
),
actions: [
businessInfo.when(
data: (business) {
return providerData.when(
data: (customers) {
final topFiveCustomers = customers.getTopFiveCustomers();
return Row(
children: [
IconButton(
onPressed: () {
if (customers.isNotEmpty) {
generateTop5CustomerReportPdf(context, topFiveCustomers, business);
} else {
EasyLoading.showError(_lang.listIsEmpty);
}
},
icon: HugeIcon(icon: HugeIcons.strokeRoundedPdf02, color: kSecondayColor),
),
/*
IconButton(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
onPressed: () {
if (!permissionService.hasPermission(Permit.expenseReportsRead.value)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text('You do not have permission to view expense report.'),
),
);
return;
}
if (customers.isNotEmpty) {
} else {
EasyLoading.showInfo('No data available for generate pdf');
}
},
icon: SvgPicture.asset('assets/excel.svg'),
),
*/
SizedBox(width: 8),
],
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: SizedBox.shrink,
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: SizedBox.shrink,
),
],
),
body: RefreshIndicator.adaptive(
onRefresh: () => refreshData(ref),
child: providerData.when(
data: (customers) {
if (!permissionService.hasPermission(Permit.partiesRead.value)) {
return const Center(child: PermitDenyWidget());
}
final topFiveCustomers = customers.getTopFiveCustomers();
return topFiveCustomers.isEmpty
? Center(child: EmptyWidget(message: TextSpan(text: l.S.of(context).noParty)))
: ListView.separated(
itemCount: topFiveCustomers.length,
padding: EdgeInsets.zero,
itemBuilder: (_, index) {
final party = topFiveCustomers[index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: GestureDetector(
// onTap: () {
// PartyLedgerScreen(
// partyId: party.id.toString(),
// partyName: party.name.toString(),
// ).launch(context);
// },
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
party.name ?? '',
style: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
Text(
'${_lang.due}: ${formatPointNumber(party.due ?? 0, addComma: true)}',
style: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
],
),
SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${_lang.type}: ${party.type ?? ''}',
style: _theme.textTheme.bodyMedium,
),
Text(
'${_lang.phone}: ${party.phone ?? 'n/a'}',
style: _theme.textTheme.bodyMedium,
),
],
),
SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${_lang.totalSales} : ${party.saleCount}',
style: _theme.textTheme.bodyMedium,
),
Text(
'${_lang.paidAmount} : ${formatPointNumber(party.totalSalePaid ?? 0, addComma: true)}',
style: _theme.textTheme.bodyMedium,
),
],
),
],
),
),
);
},
separatorBuilder: (_, __) => Divider(
thickness: 1,
height: 1,
color: kBottomBorder,
),
);
},
error: (e, stack) => Text(e.toString()),
loading: () => const Center(child: CircularProgressIndicator()),
),
),
),
);
},
error: (e, stack) => Text(e.toString()),
loading: () => const Center(child: CircularProgressIndicator()),
);
},
);
}
}

View File

@@ -0,0 +1,224 @@
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hugeicons/hugeicons.dart';
import 'package:mobile_pos/Screens/Customers/Model/parties_model.dart';
import 'package:mobile_pos/generated/l10n.dart' as l;
import 'package:nb_utils/nb_utils.dart';
import '../../../GlobalComponents/glonal_popup.dart';
import '../../../Provider/profile_provider.dart';
import '../../../constant.dart';
import '../../../pdf_report/party/top_5_supplier_report_pdf.dart';
import '../../../service/check_user_role_permission_provider.dart';
import '../../../widgets/empty_widget/_empty_widget.dart';
import '../../Customers/Provider/customer_provider.dart';
import '../../party ledger/single_party_ledger_screen.dart';
class TopFiveSupplier extends ConsumerStatefulWidget {
const TopFiveSupplier({super.key});
@override
ConsumerState<TopFiveSupplier> createState() => _CustomerLedgerReportState();
}
class _CustomerLedgerReportState extends ConsumerState<TopFiveSupplier> {
bool _isRefreshing = false;
final searchController = TextEditingController();
Future<void> refreshData(WidgetRef ref) async {
if (_isRefreshing) return;
_isRefreshing = true;
ref.refresh(partiesProvider);
await Future.delayed(const Duration(seconds: 1));
_isRefreshing = false;
}
@override
Widget build(BuildContext context) {
final _lang = l.S.of(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);
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.top5Supplier,
),
actions: [
businessInfo.when(
data: (business) {
return providerData.when(
data: (suppliers) {
final topFiveCustomers = suppliers.getTopFiveSuppliers();
return Row(
children: [
IconButton(
onPressed: () {
if (suppliers.isNotEmpty) {
generateTop5SupplierReportPdf(context, topFiveCustomers, business);
} else {
EasyLoading.showError(_lang.listIsEmpty);
}
},
icon: HugeIcon(icon: HugeIcons.strokeRoundedPdf02, color: kSecondayColor),
),
/*
IconButton(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
onPressed: () {
if (!permissionService.hasPermission(Permit.expenseReportsRead.value)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text('You do not have permission to view expense report.'),
),
);
return;
}
if (customers.isNotEmpty) {
} else {
EasyLoading.showInfo('No data available for generate pdf');
}
},
icon: SvgPicture.asset('assets/excel.svg'),
),
*/
SizedBox(width: 8),
],
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: SizedBox.shrink,
);
},
error: (e, stack) => Center(
child: Text(e.toString()),
),
loading: SizedBox.shrink,
),
],
),
body: RefreshIndicator.adaptive(
onRefresh: () => refreshData(ref),
child: providerData.when(
data: (partyList) {
if (!permissionService.hasPermission(Permit.partiesRead.value)) {
return const Center(child: PermitDenyWidget());
}
final suppliers = partyList.where((party) {
final type = (party.type ?? '').toLowerCase();
return type == 'supplier';
}).toList();
suppliers.sort((a, b) {
final aPurchase = a.purchaseCount ?? 0;
final bPurchase = b.purchaseCount ?? 0;
return bPurchase.compareTo(aPurchase);
});
final topFiveSupplier = suppliers.length > 5 ? suppliers.take(5).toList() : suppliers;
return topFiveSupplier.isEmpty
? Center(child: EmptyWidget(message: TextSpan(text: l.S.of(context).noParty)))
: ListView.separated(
itemCount: topFiveSupplier.length,
padding: EdgeInsets.zero,
itemBuilder: (_, index) {
final party = topFiveSupplier[index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: GestureDetector(
onTap: () {
PartyLedgerScreen(
partyId: party.id.toString(),
partyName: party.name.toString(),
).launch(context);
},
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
party.name ?? '',
style: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
Text(
'${_lang.due}: ${party.due}',
style: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
],
),
SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${_lang.type}: ${party.type ?? ''}',
style: _theme.textTheme.bodyMedium,
),
Text(
'${_lang.phone}: ${party.phone ?? 'n/a'}',
style: _theme.textTheme.bodyMedium,
),
],
),
SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${_lang.totalSales} : ${party.purchaseCount ?? 0}',
style: _theme.textTheme.bodyMedium,
),
Text(
'${_lang.paidAmount} : ${party.totalPurchasePaid ?? 0}',
style: _theme.textTheme.bodyMedium,
),
],
),
],
),
),
);
},
separatorBuilder: (_, __) => Divider(
thickness: 1,
height: 1,
color: kBottomBorder,
),
);
},
error: (e, stack) => Text(e.toString()),
loading: () => const Center(child: CircularProgressIndicator()),
),
),
),
);
},
error: (e, stack) => Text(e.toString()),
loading: () => const Center(child: CircularProgressIndicator()),
);
},
);
}
}