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,532 @@
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:iconly/iconly.dart';
import 'package:intl/intl.dart';
import 'package:mobile_pos/generated/l10n.dart' as l;
import 'package:mobile_pos/Provider/transactions_provider.dart';
import '../../../Provider/profile_provider.dart';
import '../../../constant.dart';
import '../../../GlobalComponents/glonal_popup.dart';
import '../../../currency.dart';
import '../../../pdf_report/transactions/balance_sheet_report_pdf.dart';
import '../../../service/check_user_role_permission_provider.dart';
class BalanceSheetScreen extends ConsumerStatefulWidget {
const BalanceSheetScreen({super.key, this.fromReport});
final bool? fromReport;
@override
ConsumerState<BalanceSheetScreen> createState() => _BalanceSheetScreenState();
}
class _BalanceSheetScreenState extends ConsumerState<BalanceSheetScreen> {
final TextEditingController fromDateController = TextEditingController();
final TextEditingController toDateController = TextEditingController();
final Map<String, String> dateOptions = {
'today': l.S.current.today,
'yesterday': l.S.current.yesterday,
'last_seven_days': l.S.current.last7Days,
'last_thirty_days': l.S.current.last30Days,
'current_month': l.S.current.currentMonth,
'last_month': l.S.current.lastMonth,
'current_year': l.S.current.currentYear,
'custom_date': l.S.current.customerDate,
};
String selectedTime = 'today';
bool _isRefreshing = false;
bool _showCustomDatePickers = false;
DateTime? fromDate;
DateTime? toDate;
String searchCustomer = '';
/// Generates the date range string for the provider
FilterModel _getDateRangeFilter() {
if (_showCustomDatePickers && fromDate != null && toDate != null) {
return FilterModel(
duration: 'custom_date',
fromDate: DateFormat('yyyy-MM-dd', 'en_US').format(fromDate!),
toDate: DateFormat('yyyy-MM-dd', 'en_US').format(toDate!),
);
} else {
return FilterModel(duration: selectedTime.toLowerCase());
}
}
Future<void> _selectDate({
required BuildContext context,
required bool isFrom,
}) async {
final DateTime? picked = await showDatePicker(
context: context,
firstDate: DateTime(2021),
lastDate: DateTime.now(),
initialDate: isFrom ? fromDate ?? DateTime.now() : toDate ?? DateTime.now(),
);
if (picked != null) {
setState(() {
if (isFrom) {
fromDate = picked;
fromDateController.text = DateFormat('yyyy-MM-dd').format(picked);
} else {
toDate = picked;
toDateController.text = DateFormat('yyyy-MM-dd').format(picked);
}
});
if (fromDate != null && toDate != null) _refreshFilteredProvider();
}
}
Future<void> _refreshFilteredProvider() async {
if (_isRefreshing) return;
_isRefreshing = true;
try {
final filter = _getDateRangeFilter();
ref.refresh(filteredBalanceSheetProvider(filter));
await Future.delayed(const Duration(milliseconds: 300)); // small delay
} finally {
_isRefreshing = false;
}
}
@override
void dispose() {
fromDateController.dispose();
toDateController.dispose();
super.dispose();
}
void _updateDateUI(DateTime? from, DateTime? to) {
setState(() {
fromDate = from;
toDate = to;
fromDateController.text = from != null ? DateFormat('yyyy-MM-dd').format(from) : '';
toDateController.text = to != null ? DateFormat('yyyy-MM-dd').format(to) : '';
});
}
void _setDateRangeFromDropdown(String value) {
final now = DateTime.now();
switch (value) {
case 'today':
_updateDateUI(now, now);
break;
case 'yesterday':
final y = now.subtract(const Duration(days: 1));
_updateDateUI(y, y);
break;
case 'last_seven_days':
_updateDateUI(
now.subtract(const Duration(days: 6)),
now,
);
break;
case 'last_thirty_days':
_updateDateUI(
now.subtract(const Duration(days: 29)),
now,
);
break;
case 'current_month':
_updateDateUI(
DateTime(now.year, now.month, 1),
now,
);
break;
case 'last_month':
final first = DateTime(now.year, now.month - 1, 1);
final last = DateTime(now.year, now.month, 0);
_updateDateUI(first, last);
break;
case 'current_year':
_updateDateUI(
DateTime(now.year, 1, 1),
now,
);
break;
case 'custom_date':
// Custom: User will select manually
_updateDateUI(null, null);
break;
}
}
@override
void initState() {
super.initState();
final now = DateTime.now();
// Set initial From and To date = TODAY
fromDate = now;
toDate = now;
fromDateController.text = DateFormat('yyyy-MM-dd').format(now);
toDateController.text = DateFormat('yyyy-MM-dd').format(now);
}
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
final _lang = l.S.of(context);
return Consumer(
builder: (_, ref, watch) {
final providerData = ref.watch(filteredBalanceSheetProvider(_getDateRangeFilter()));
final personalData = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
return personalData.when(
data: (business) {
return providerData.when(
data: (transaction) {
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
backgroundColor: Colors.white,
title: Text(_lang.balanceSheet),
actions: [
IconButton(
onPressed: () {
if (transaction.data?.isNotEmpty == true) {
generateBalanceSheetReportPdf(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showError(_lang.listIsEmpty);
}
},
icon: HugeIcon(icon: HugeIcons.strokeRoundedPdf02, color: kSecondayColor),
),
/*
IconButton(
onPressed: () {
if (!permissionService.hasPermission(Permit.lossProfitsRead.value)) {
// TODO: Shakil fix this permission
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text('You do not have permission of loss profit.'),
),
);
return;
}
if ((transaction.expenseSummary?.isNotEmpty == true) ||
(transaction.incomeSummary?.isNotEmpty == true)) {
generateLossProfitReportExcel(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showError('List is empty');
}
},
icon: SvgPicture.asset('assets/excel.svg'),
),
*/
SizedBox(width: 8),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(50),
child: Column(
children: [
Divider(thickness: 1, color: kBottomBorder, height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
flex: 2,
child: Row(
children: [
Icon(IconlyLight.calendar, color: kPeraColor, size: 20),
SizedBox(width: 3),
GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: true);
}
},
child: Text(
fromDate != null ? DateFormat('dd MMM yyyy').format(fromDate!) : _lang.from,
style: Theme.of(context).textTheme.bodyMedium,
),
),
SizedBox(width: 4),
Text(
_lang.to,
style: _theme.textTheme.titleSmall,
),
SizedBox(width: 4),
Flexible(
child: GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: false);
}
},
child: Text(
toDate != null ? DateFormat('dd MMM yyyy').format(toDate!) : _lang.to,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
],
),
),
SizedBox(width: 2),
RotatedBox(
quarterTurns: 1,
child: Container(
height: 1,
width: 20,
color: kSubPeraColor,
),
),
SizedBox(width: 2),
Expanded(
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
iconSize: 20,
value: selectedTime,
isExpanded: true,
items: dateOptions.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(
entry.value,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium,
),
);
}).toList(),
onChanged: (value) {
if (value == null) return;
setState(() {
selectedTime = value;
_showCustomDatePickers = value == 'custom_date';
});
if (value != 'custom_date') {
_setDateRangeFromDropdown(value);
_refreshFilteredProvider();
}
},
),
),
),
],
),
),
Divider(thickness: 1, color: kBottomBorder, height: 1),
],
),
),
iconTheme: const IconThemeData(color: Colors.black),
centerTitle: true,
elevation: 0.0,
),
body: RefreshIndicator(
onRefresh: _refreshFilteredProvider,
child: Column(
children: [
// Overview Containers
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Expanded(
child: Container(
height: 77,
decoration: BoxDecoration(
color: kSuccessColor.withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(transaction.totalAsset ?? 0)}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
_lang.totalAssets,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
),
/*
SizedBox(width: 12),
Expanded(
child: Container(
height: 77,
width: double.infinity,
decoration: BoxDecoration(
color: DAppColors.kError.withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(transaction.netProfit ?? 0)}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
"Liabilities",
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
),
*/
],
),
),
// Data
Expanded(
child: ListView(
children: [
// Header
DefaultTextStyle.merge(
style: _theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xffF7F7F7),
border: Border(bottom: Divider.createBorderSide(context)),
),
child: Row(
children: [
Expanded(child: Text(_lang.name)),
Flexible(flex: 0, child: Text(_lang.amount, textAlign: TextAlign.end)),
],
),
),
),
// Sub Header
DefaultTextStyle.merge(
style: _theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w600,
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
width: double.maxFinite,
decoration: BoxDecoration(
border: Border(bottom: Divider.createBorderSide(context)),
),
child: Text(_lang.assets),
),
),
// Item
...?transaction.data?.map((incomeType) {
return DefaultTextStyle.merge(
style: _theme.textTheme.bodyMedium,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
border: Border(bottom: Divider.createBorderSide(context)),
),
child: Row(
children: [
Expanded(child: Text(incomeType.name ?? 'N/A')),
Flexible(
flex: 0,
child: Text(
"$currency${formatPointNumber(incomeType.amount ?? 0, addComma: true)}",
textAlign: TextAlign.end,
),
),
],
),
),
);
}),
// Footer
DefaultTextStyle.merge(
style: _theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w600,
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xffFEF0F1),
border: Border(bottom: Divider.createBorderSide(context)),
),
child: Row(
children: [
Expanded(child: Text(_lang.total)),
Flexible(
flex: 0,
child: Text(
"$currency${formatPointNumber(transaction.totalAsset ?? 0, addComma: true)}",
textAlign: TextAlign.end,
),
),
],
),
),
),
],
),
)
],
),
),
),
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: () => Center(child: CircularProgressIndicator()),
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: () => Center(child: CircularProgressIndicator()),
);
},
);
}
}

View File

@@ -0,0 +1,731 @@
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:iconly/iconly.dart';
import 'package:intl/intl.dart';
import 'package:mobile_pos/Provider/transactions_provider.dart';
import 'package:mobile_pos/generated/l10n.dart' as l;
import '../../../Provider/profile_provider.dart';
import '../../../constant.dart';
import '../../../GlobalComponents/glonal_popup.dart';
import '../../../core/theme/_app_colors.dart';
import '../../../currency.dart';
import '../../../model/bill_wise_loss_profit_report_model.dart' as bwlpm;
import '../../../pdf_report/loss_profit_report/bill_wise_loss_profit_report_pdf.dart';
import '../../../service/check_user_role_permission_provider.dart';
class BillWiseProfitScreen extends ConsumerStatefulWidget {
const BillWiseProfitScreen({super.key});
@override
ConsumerState<BillWiseProfitScreen> createState() => _BillWiseProfitScreenState();
}
class _BillWiseProfitScreenState extends ConsumerState<BillWiseProfitScreen> {
final TextEditingController fromDateController = TextEditingController();
final TextEditingController toDateController = TextEditingController();
final Map<String, String> dateOptions = {
'today': l.S.current.today,
'yesterday': l.S.current.yesterday,
'last_seven_days': l.S.current.last7Days,
'last_thirty_days': l.S.current.last30Days,
'current_month': l.S.current.currentMonth,
'last_month': l.S.current.lastMonth,
'current_year': l.S.current.currentYear,
'custom_date': l.S.current.customerDate,
};
String selectedTime = 'today';
bool _isRefreshing = false;
bool _showCustomDatePickers = false;
DateTime? fromDate;
DateTime? toDate;
String searchCustomer = '';
/// Generates the date range string for the provider
FilterModel _getDateRangeFilter() {
if (_showCustomDatePickers && fromDate != null && toDate != null) {
return FilterModel(
duration: 'custom_date',
fromDate: DateFormat('yyyy-MM-dd', 'en_US').format(fromDate!),
toDate: DateFormat('yyyy-MM-dd', 'en_US').format(toDate!),
);
} else {
return FilterModel(duration: selectedTime.toLowerCase());
}
}
Future<void> _selectDate({
required BuildContext context,
required bool isFrom,
}) async {
final DateTime? picked = await showDatePicker(
context: context,
firstDate: DateTime(2021),
lastDate: DateTime.now(),
initialDate: isFrom ? fromDate ?? DateTime.now() : toDate ?? DateTime.now(),
);
if (picked != null) {
setState(() {
if (isFrom) {
fromDate = picked;
fromDateController.text = DateFormat('yyyy-MM-dd').format(picked);
} else {
toDate = picked;
toDateController.text = DateFormat('yyyy-MM-dd').format(picked);
}
});
if (fromDate != null && toDate != null) _refreshFilteredProvider();
}
}
Future<void> _refreshFilteredProvider() async {
if (_isRefreshing) return;
_isRefreshing = true;
try {
final filter = _getDateRangeFilter();
ref.refresh(filteredBillWiseLossProfitReportProvider(filter));
await Future.delayed(const Duration(milliseconds: 300)); // small delay
} finally {
_isRefreshing = false;
}
}
@override
void dispose() {
fromDateController.dispose();
toDateController.dispose();
super.dispose();
}
void _updateDateUI(DateTime? from, DateTime? to) {
setState(() {
fromDate = from;
toDate = to;
fromDateController.text = from != null ? DateFormat('yyyy-MM-dd').format(from) : '';
toDateController.text = to != null ? DateFormat('yyyy-MM-dd').format(to) : '';
});
}
void _setDateRangeFromDropdown(String value) {
final now = DateTime.now();
switch (value) {
case 'today':
_updateDateUI(now, now);
break;
case 'yesterday':
final y = now.subtract(const Duration(days: 1));
_updateDateUI(y, y);
break;
case 'last_seven_days':
_updateDateUI(
now.subtract(const Duration(days: 6)),
now,
);
break;
case 'last_thirty_days':
_updateDateUI(
now.subtract(const Duration(days: 29)),
now,
);
break;
case 'current_month':
_updateDateUI(
DateTime(now.year, now.month, 1),
now,
);
break;
case 'last_month':
final first = DateTime(now.year, now.month - 1, 1);
final last = DateTime(now.year, now.month, 0);
_updateDateUI(first, last);
break;
case 'current_year':
_updateDateUI(
DateTime(now.year, 1, 1),
now,
);
break;
case 'custom_date':
// Custom: User will select manually
_updateDateUI(null, null);
break;
}
}
@override
void initState() {
super.initState();
final now = DateTime.now();
// Set initial From and To date = TODAY
fromDate = now;
toDate = now;
fromDateController.text = DateFormat('yyyy-MM-dd').format(now);
toDateController.text = DateFormat('yyyy-MM-dd').format(now);
}
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
final _lang = l.S.of(context);
return Consumer(
builder: (_, ref, watch) {
final providerData = ref.watch(filteredBillWiseLossProfitReportProvider(_getDateRangeFilter()));
final personalData = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
return personalData.when(
data: (business) {
return providerData.when(
data: (data) {
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
backgroundColor: Colors.white,
title: Text(_lang.billWiseProfit),
actions: [
IconButton(
onPressed: () {
if (data.transactions?.isNotEmpty == true) {
generateBillWiseLossProfitReportPdf(context, data, business, fromDate, toDate);
} else {
EasyLoading.showError(_lang.listIsEmpty);
}
},
icon: HugeIcon(icon: HugeIcons.strokeRoundedPdf02, color: kSecondayColor),
),
/*
IconButton(
onPressed: () {
if (!permissionService.hasPermission(Permit.lossProfitsRead.value)) {
// TODO: Shakil fix this permission
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text('You do not have permission of loss profit.'),
),
);
return;
}
if ((transaction.expenseSummary?.isNotEmpty == true) ||
(transaction.incomeSummary?.isNotEmpty == true)) {
generateLossProfitReportExcel(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showError('List is empty');
}
},
icon: SvgPicture.asset('assets/excel.svg'),
),
*/
SizedBox(width: 8),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(50),
child: Column(
children: [
Divider(thickness: 1, color: kBottomBorder, height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
flex: 2,
child: Row(
children: [
Icon(IconlyLight.calendar, color: kPeraColor, size: 20),
SizedBox(width: 3),
GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: true);
}
},
child: Text(
fromDate != null ? DateFormat('dd MMM yyyy').format(fromDate!) : _lang.from,
style: Theme.of(context).textTheme.bodyMedium,
),
),
SizedBox(width: 4),
Text(
_lang.to,
style: _theme.textTheme.titleSmall,
),
SizedBox(width: 4),
Flexible(
child: GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: false);
}
},
child: Text(
toDate != null ? DateFormat('dd MMM yyyy').format(toDate!) : _lang.to,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
],
),
),
SizedBox(width: 2),
RotatedBox(
quarterTurns: 1,
child: Container(
height: 1,
width: 20,
color: kSubPeraColor,
),
),
SizedBox(width: 2),
Expanded(
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
iconSize: 20,
value: selectedTime,
isExpanded: true,
items: dateOptions.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(
entry.value,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium,
),
);
}).toList(),
onChanged: (value) {
if (value == null) return;
setState(() {
selectedTime = value;
_showCustomDatePickers = value == 'custom_date';
});
if (value != 'custom_date') {
_setDateRangeFromDropdown(value);
_refreshFilteredProvider();
}
},
),
),
),
],
),
),
Divider(thickness: 1, color: kBottomBorder, height: 1),
],
),
),
iconTheme: const IconThemeData(color: Colors.black),
centerTitle: true,
elevation: 0.0,
),
body: RefreshIndicator(
onRefresh: _refreshFilteredProvider,
child: Column(
children: [
// Overview Containers
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Expanded(
child: Container(
height: 77,
decoration: BoxDecoration(
color: kSuccessColor.withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(data.totalProfit ?? 0)}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
_lang.profit,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
),
SizedBox(width: 12),
Expanded(
child: Container(
height: 77,
width: double.infinity,
decoration: BoxDecoration(
color: DAppColors.kError.withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber((data.totalLoss ?? 0).abs())}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
_lang.loss,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
),
],
),
),
// Data
Expanded(
child: ListView.builder(
itemCount: data.transactions?.length ?? 0,
itemBuilder: (context, index) {
final transaction = [...?data.transactions][index];
return GestureDetector(
onTap: () => handleShowInvoiceDetails(context, transaction),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
border: Border(bottom: Divider.createBorderSide(context)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: DefaultTextStyle.merge(
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 15,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 2,
children: [
Text(
transaction.partyName ?? "N/A",
style: TextStyle(fontWeight: FontWeight.w600),
),
Text(transaction.invoiceNumber ?? "N/A"),
Text(
transaction.transactionDate == null
? "N/A"
: DateFormat('dd MMM yyyy')
.format(transaction.transactionDate!),
style: TextStyle(color: const Color(0xff4B5563)),
),
],
),
),
),
Expanded(
child: DefaultTextStyle.merge(
overflow: TextOverflow.ellipsis,
maxLines: 1,
textAlign: TextAlign.end,
style: _theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 15,
color: const Color(0xff4B5563),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
spacing: 2,
children: [
Text(
'${_lang.sales}: $currency${formatPointNumber(transaction.totalAmount ?? 0, addComma: true)}',
),
Text.rich(
TextSpan(
text: "${_lang.profit}: ",
children: [
TextSpan(
text:
'$currency${formatPointNumber(transaction.isProfit ? (transaction.lossProfit ?? 0) : 0, addComma: true)}',
style: TextStyle(
color: transaction.isProfit ? DAppColors.kSuccess : null,
),
)
],
),
),
Text.rich(
TextSpan(
text: "${_lang.loss}: ",
children: [
TextSpan(
text:
'$currency${formatPointNumber(transaction.isProfit ? 0 : (transaction.lossProfit ?? 0).abs(), addComma: true)}',
style: TextStyle(
color: transaction.isProfit ? null : DAppColors.kError,
),
)
],
),
),
],
),
),
),
],
)
],
),
),
);
},
),
)
],
),
),
),
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: () => Center(child: CircularProgressIndicator()),
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: () => Center(child: CircularProgressIndicator()),
);
},
);
}
Future<void> handleShowInvoiceDetails(BuildContext context, bwlpm.TransactionModel transaction) async {
return showModalBottomSheet<void>(
context: context,
builder: (modalContext) => TestModal(transaction: transaction),
);
}
}
class TestModal extends StatelessWidget {
const TestModal({super.key, required this.transaction});
final bwlpm.TransactionModel transaction;
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
final _lang = l.S.of(context);
final locale = Localizations.localeOf(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Container(
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
decoration: BoxDecoration(
border: Border(bottom: Divider.createBorderSide(context)),
),
child: Row(
children: [
Expanded(
child: Text(
'${_lang.invoice}: ${transaction.invoiceNumber ?? "N/A"} - ${transaction.partyName ?? ""}',
style: _theme.textTheme.titleMedium?.copyWith(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
const CloseButton(),
],
),
),
Flexible(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(12),
color: const Color(0xffF5F3F3),
child: Row(
children: [
...[
_lang.itemName,
_lang.qty,
locale.languageCode == 'en' ? "Purch" : _lang.purchase,
_lang.salePrice,
_lang.profit,
_lang.loss,
].asMap().entries.map((entry) {
return Expanded(
flex: entry.key == 0 ? 4 : 3,
child: Text(
entry.value,
textAlign: entry.key == 0 ? TextAlign.start : TextAlign.center,
style: _theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
);
})
],
),
),
Flexible(
child: ListView.builder(
shrinkWrap: true,
itemCount: transaction.items?.length ?? 0,
itemBuilder: (context, index) {
final _item = [...?transaction.items][index];
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border(bottom: Divider.createBorderSide(context)),
),
child: Row(
children: [
Expanded(
flex: 4,
child: Text(
_item.name ?? "N/A",
textAlign: TextAlign.start,
style: _theme.textTheme.bodyMedium?.copyWith(),
),
),
Expanded(
flex: 3,
child: Text(
(_item.quantity ?? 0).toString(),
textAlign: TextAlign.center,
style: _theme.textTheme.bodyMedium?.copyWith(),
),
),
Expanded(
flex: 3,
child: Text(
"$currency${formatPointNumber(_item.purchasePrice ?? 0, addComma: true)}",
textAlign: TextAlign.center,
style: _theme.textTheme.bodyMedium?.copyWith(),
),
),
Expanded(
flex: 3,
child: Text(
"$currency${formatPointNumber(_item.salesPrice ?? 0, addComma: true)}",
textAlign: TextAlign.center,
style: _theme.textTheme.bodyMedium?.copyWith(),
),
),
Expanded(
flex: 3,
child: Text(
"$currency${formatPointNumber(_item.isProfit ? (_item.lossProfit ?? 0) : 0, addComma: true)}",
textAlign: TextAlign.center,
style: _theme.textTheme.bodyMedium?.copyWith(
color: _item.isProfit ? DAppColors.kSuccess : null,
),
),
),
Expanded(
flex: 3,
child: Text(
"$currency${formatPointNumber(_item.isProfit ? 0 : (_item.lossProfit ?? 0).abs(), addComma: true)}",
textAlign: TextAlign.center,
style: _theme.textTheme.bodyMedium?.copyWith(
color: _item.isProfit ? null : DAppColors.kError,
),
),
),
],
),
);
},
),
)
],
),
),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border(bottom: Divider.createBorderSide(context)),
),
child: Row(
children: [
...[
_lang.total,
"${transaction.items?.fold<int>(0, (p, ev) => p + (ev.quantity ?? 0))}",
"--",
"--",
"$currency${formatPointNumber(transaction.items?.fold<num>(0, (p, ev) {
return ev.isProfit ? (p + (ev.lossProfit ?? 0)) : p;
}) ?? 0, addComma: true)}",
"$currency${formatPointNumber(transaction.items?.fold<num>(0, (p, ev) {
return ev.isProfit ? p : (p + (ev.lossProfit ?? 0));
}).abs() ?? 0, addComma: true)}",
].asMap().entries.map((entry) {
return Expanded(
flex: entry.key == 0 ? 4 : 3,
child: Text(
entry.value,
textAlign: entry.key == 0 ? TextAlign.start : TextAlign.center,
style: _theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
);
})
],
),
),
const SizedBox.square(dimension: 16),
],
);
}
}

View File

@@ -0,0 +1,631 @@
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:iconly/iconly.dart';
import 'package:intl/intl.dart';
import 'package:intl/intl.dart' as intl;
import 'package:nb_utils/nb_utils.dart';
import 'package:mobile_pos/generated/l10n.dart' as l;
import '../../../GlobalComponents/glonal_popup.dart';
import '../../../Provider/profile_provider.dart';
import '../../../Provider/transactions_provider.dart';
import '../../../constant.dart';
import '../../../core/theme/_app_colors.dart';
import '../../../currency.dart';
import '../../../pdf_report/transactions/cashflow_report_pdf.dart';
import '../../../service/check_user_role_permission_provider.dart';
import '../../Home/home.dart';
class CashflowScreen extends ConsumerStatefulWidget {
const CashflowScreen({super.key, this.fromReport});
final bool? fromReport;
@override
ConsumerState<CashflowScreen> createState() => _CashflowScreenState();
}
class _CashflowScreenState extends ConsumerState<CashflowScreen> {
final TextEditingController fromDateController = TextEditingController();
final TextEditingController toDateController = TextEditingController();
final selectedTransactionTypeNotifier = ValueNotifier<String>('debit');
final Map<String, String> dateOptions = {
'today': l.S.current.today,
'yesterday': l.S.current.yesterday,
'last_seven_days': l.S.current.last7Days,
'last_thirty_days': l.S.current.last30Days,
'current_month': l.S.current.currentMonth,
'last_month': l.S.current.lastMonth,
'current_year': l.S.current.currentYear,
'custom_date': l.S.current.customerDate,
};
String selectedTime = 'today';
bool _isRefreshing = false;
bool _showCustomDatePickers = false;
DateTime? fromDate;
DateTime? toDate;
String searchCustomer = '';
/// Generates the date range string for the provider
FilterModel _getDateRangeFilter() {
if (_showCustomDatePickers && fromDate != null && toDate != null) {
return FilterModel(
duration: 'custom_date',
fromDate: DateFormat('yyyy-MM-dd', 'en_US').format(fromDate!),
toDate: DateFormat('yyyy-MM-dd', 'en_US').format(toDate!),
);
} else {
return FilterModel(duration: selectedTime.toLowerCase());
}
}
Future<void> _selectDate({
required BuildContext context,
required bool isFrom,
}) async {
final DateTime? picked = await showDatePicker(
context: context,
firstDate: DateTime(2021),
lastDate: DateTime.now(),
initialDate: isFrom ? fromDate ?? DateTime.now() : toDate ?? DateTime.now(),
);
if (picked != null) {
setState(() {
if (isFrom) {
fromDate = picked;
fromDateController.text = DateFormat('yyyy-MM-dd').format(picked);
} else {
toDate = picked;
toDateController.text = DateFormat('yyyy-MM-dd').format(picked);
}
});
if (fromDate != null && toDate != null) _refreshFilteredProvider();
}
}
Future<void> _refreshFilteredProvider() async {
if (_isRefreshing) return;
_isRefreshing = true;
try {
final filter = _getDateRangeFilter();
ref.refresh(filteredCashflowProvider(filter));
await Future.delayed(const Duration(milliseconds: 300)); // small delay
} finally {
_isRefreshing = false;
}
}
@override
void dispose() {
fromDateController.dispose();
toDateController.dispose();
super.dispose();
}
void _updateDateUI(DateTime? from, DateTime? to) {
setState(() {
fromDate = from;
toDate = to;
fromDateController.text = from != null ? DateFormat('yyyy-MM-dd').format(from) : '';
toDateController.text = to != null ? DateFormat('yyyy-MM-dd').format(to) : '';
});
}
void _setDateRangeFromDropdown(String value) {
final now = DateTime.now();
switch (value) {
case 'today':
_updateDateUI(now, now);
break;
case 'yesterday':
final y = now.subtract(const Duration(days: 1));
_updateDateUI(y, y);
break;
case 'last_seven_days':
_updateDateUI(
now.subtract(const Duration(days: 6)),
now,
);
break;
case 'last_thirty_days':
_updateDateUI(
now.subtract(const Duration(days: 29)),
now,
);
break;
case 'current_month':
_updateDateUI(
DateTime(now.year, now.month, 1),
now,
);
break;
case 'last_month':
final first = DateTime(now.year, now.month - 1, 1);
final last = DateTime(now.year, now.month, 0);
_updateDateUI(first, last);
break;
case 'current_year':
_updateDateUI(
DateTime(now.year, 1, 1),
now,
);
break;
case 'custom_date':
// Custom: User will select manually
_updateDateUI(null, null);
break;
}
}
@override
void initState() {
super.initState();
final now = DateTime.now();
// Set initial From and To date = TODAY
fromDate = now;
toDate = now;
fromDateController.text = DateFormat('yyyy-MM-dd').format(now);
toDateController.text = DateFormat('yyyy-MM-dd').format(now);
}
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
return WillPopScope(
onWillPop: () async {
return await const Home().launch(context, isNewTask: true);
},
child: Consumer(
builder: (_, ref, watch) {
final _lang = l.S.of(context);
final providerData = ref.watch(filteredCashflowProvider(_getDateRangeFilter()));
final personalData = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
return personalData.when(
data: (business) {
return providerData.when(
data: (tx) {
final _transactions = [...?tx.data];
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
backgroundColor: Colors.white,
title: Text(_lang.cashFlow),
actions: [
IconButton(
onPressed: () {
if ((tx.data?.isNotEmpty == true) || (tx.data?.isNotEmpty == true)) {
generateCashflowReportPdf(context, tx, business, fromDate, toDate);
} else {
EasyLoading.showError(_lang.listIsEmpty);
}
},
icon: HugeIcon(icon: HugeIcons.strokeRoundedPdf02, color: kSecondayColor),
),
IconButton(
onPressed: () {
if ((tx.data?.isNotEmpty == true) || (tx.data?.isNotEmpty == true)) {
// generateLossProfitReportExcel(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showError(_lang.listIsEmpty);
}
},
icon: SvgPicture.asset('assets/excel.svg'),
),
SizedBox(width: 8),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(50),
child: Column(
children: [
Divider(thickness: 1, color: kBottomBorder, height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
flex: 2,
child: Row(
children: [
Icon(IconlyLight.calendar, color: kPeraColor, size: 20),
SizedBox(width: 3),
GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: true);
}
},
child: Text(
fromDate != null
? DateFormat('dd MMM yyyy').format(fromDate!)
: _lang.from,
style: Theme.of(context).textTheme.bodyMedium,
),
),
SizedBox(width: 4),
Text(
_lang.to,
style: _theme.textTheme.titleSmall,
),
SizedBox(width: 4),
Flexible(
child: GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: false);
}
},
child: Text(
toDate != null ? DateFormat('dd MMM yyyy').format(toDate!) : _lang.to,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
],
),
),
SizedBox(width: 2),
RotatedBox(
quarterTurns: 1,
child: Container(
height: 1,
width: 20,
color: kSubPeraColor,
),
),
SizedBox(width: 2),
Expanded(
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
iconSize: 20,
value: selectedTime,
isExpanded: true,
items: dateOptions.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(
entry.value,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium,
),
);
}).toList(),
onChanged: (value) {
if (value == null) return;
setState(() {
selectedTime = value;
_showCustomDatePickers = value == 'custom_date';
});
if (value != 'custom_date') {
_setDateRangeFromDropdown(value);
_refreshFilteredProvider();
}
},
),
),
),
],
),
),
Divider(thickness: 1, color: kBottomBorder, height: 1),
],
),
),
iconTheme: const IconThemeData(color: Colors.black),
centerTitle: true,
elevation: 0.0,
),
body: RefreshIndicator(
onRefresh: _refreshFilteredProvider,
child: Column(
children: [
// Overview Containers
SizedBox.fromSize(
size: Size.fromHeight(100),
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
scrollDirection: Axis.horizontal,
child: Row(
children: [
Container(
constraints: const BoxConstraints(minWidth: 170, maxHeight: 80),
decoration: BoxDecoration(
color: kSuccessColor.withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(tx.cashIn ?? 0)}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
_lang.cashIn,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
SizedBox(width: 12),
Container(
constraints: const BoxConstraints(minWidth: 170, maxHeight: 80),
decoration: BoxDecoration(
color: DAppColors.kError.withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(tx.cashOut ?? 0)}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
_lang.cashOut,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
SizedBox(width: 12),
Container(
constraints: const BoxConstraints(minWidth: 170, maxHeight: 80),
decoration: BoxDecoration(
color: const Color(0xffFAE3FF),
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(tx.runningCash ?? 0)}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
_lang.runningCash,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
],
),
),
),
// Data
Expanded(
child: DefaultTabController(
length: 2,
child: Builder(
builder: (tabContext) {
DefaultTabController.of(tabContext).addListener(
() {
selectedTransactionTypeNotifier.value =
['credit', 'debit'][DefaultTabController.of(tabContext).index];
},
);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox.fromSize(
size: const Size.fromHeight(40),
child: TabBar(
indicatorSize: TabBarIndicatorSize.tab,
unselectedLabelColor: const Color(0xff4B5563),
tabs: [
Tab(text: _lang.cashIn),
Tab(text: _lang.cashOut),
],
),
),
Expanded(
child: ValueListenableBuilder(
valueListenable: selectedTransactionTypeNotifier,
builder: (_, selectedTransactionType, __) {
final _filteredTransactions = _transactions
.where((element) => element.type == selectedTransactionType)
.toList();
return Column(
mainAxisSize: MainAxisSize.min,
children: [
DefaultTextStyle.merge(
style: _theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 15,
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xffF7F7F7),
border: Border(bottom: Divider.createBorderSide(context)),
),
child: Row(
children: [
Expanded(
flex: 4,
child: Text(_lang.name, textAlign: TextAlign.start),
),
Expanded(
flex: 3,
child: Text(_lang.type, textAlign: TextAlign.center),
),
Expanded(
flex: 2,
child: Text(
selectedTransactionType == "credit"
? _lang.cashIn
: _lang.cashOut,
textAlign: TextAlign.end,
),
),
],
),
),
),
Expanded(
child: ListView.builder(
itemCount: _filteredTransactions.length,
itemBuilder: (context, index) {
final _transaction = _filteredTransactions[index];
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 6,
),
decoration: BoxDecoration(
border: Border(
bottom: Divider.createBorderSide(context),
),
),
child: Row(
children: [
Expanded(
flex: 4,
child: Text.rich(
TextSpan(
text: "${_transaction.partyName ?? "N/A"}\n",
children: [
TextSpan(
text: _transaction.date == null
? "N/A"
: intl.DateFormat("dd MMM yyyy")
.format(_transaction.date!),
style: TextStyle(
fontWeight: FontWeight.normal,
color: const Color(0xff4B5563),
),
),
],
),
style: _theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
),
Expanded(
flex: 3,
child: Text(
_transaction.platform?.toTitleCase() ?? "N/A",
textAlign: TextAlign.center,
),
),
Expanded(
flex: 2,
child: Text(
"$currency${formatPointNumber(_transaction.amount ?? 0, addComma: true)}",
textAlign: TextAlign.end,
),
),
],
),
);
},
),
)
],
);
},
),
)
],
);
},
),
),
)
],
),
),
),
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: () => Center(child: CircularProgressIndicator()),
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: () => Center(child: CircularProgressIndicator()),
);
},
),
);
}
}
extension TitleCaseExtension on String {
String toTitleCase() {
if (isEmpty) return this;
final normalized = replaceAll(RegExp(r'[_\-]+'), ' ');
final words = normalized.split(' ').map((w) => w.trim()).where((w) => w.isNotEmpty).toList();
if (words.isEmpty) return '';
final titleCased = words.map((word) {
final lower = word.toLowerCase();
return lower[0].toUpperCase() + lower.substring(1);
}).join(' ');
return titleCased;
}
}

View File

@@ -0,0 +1,614 @@
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:iconly/iconly.dart';
import 'package:intl/intl.dart';
import 'package:intl/intl.dart' as intl; // Alias for date formatting inside list
import 'package:mobile_pos/core/theme/_app_colors.dart';
import 'package:mobile_pos/currency.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/transactions/daybook_report_pdf.dart';
import '../../../service/check_user_role_permission_provider.dart';
import '../../all_transaction/model/transaction_model.dart';
import '../../all_transaction/provider/transacton_provider.dart';
class DayBookReport extends ConsumerStatefulWidget {
const DayBookReport({super.key});
@override
DayBookReportState createState() => DayBookReportState();
}
class DayBookReportState extends ConsumerState<DayBookReport> {
final TextEditingController fromDateController = TextEditingController();
final TextEditingController toDateController = TextEditingController();
// Logic to switch between Credit/Debit in the list
final selectedTransactionTypeNotifier = ValueNotifier<String>('credit');
final Map<String, String> dateOptions = {
'today': l.S.current.today,
'yesterday': l.S.current.yesterday,
'last_seven_days': l.S.current.last7Days,
'last_thirty_days': l.S.current.last30Days,
'current_month': l.S.current.currentMonth,
'last_month': l.S.current.lastMonth,
'current_year': l.S.current.currentYear,
'custom_date': l.S.current.customerDate,
};
String selectedTime = 'today';
bool _isRefreshing = false;
bool _showCustomDatePickers = false;
DateTime? fromDate;
DateTime? toDate;
/// Generates the date range string for the provider
TransactionFilteredModel _getDateRangeFilter() {
if (_showCustomDatePickers && fromDate != null && toDate != null) {
return TransactionFilteredModel(
duration: 'custom_date',
fromDate: DateFormat('yyyy-MM-dd', 'en_US').format(fromDate!),
toDate: DateFormat('yyyy-MM-dd', 'en_US').format(toDate!),
);
} else {
// For predefined ranges (today, yesterday, etc.)
return TransactionFilteredModel(
duration: selectedTime.toLowerCase(),
);
}
}
Future<void> _selectDate({
required BuildContext context,
required bool isFrom,
}) async {
final DateTime? picked = await showDatePicker(
context: context,
firstDate: DateTime(2021),
lastDate: DateTime.now(),
initialDate: isFrom ? fromDate ?? DateTime.now() : toDate ?? DateTime.now(),
);
if (picked != null) {
setState(() {
if (isFrom) {
fromDate = picked;
fromDateController.text = DateFormat('yyyy-MM-dd').format(picked);
} else {
toDate = picked;
toDateController.text = DateFormat('yyyy-MM-dd').format(picked);
}
});
// If custom date is selected and both dates are present, refresh
if (selectedTime == 'custom_date' && fromDate != null && toDate != null) {
_refreshFilteredProvider();
}
}
}
Future<void> _refreshFilteredProvider() async {
if (_isRefreshing) return;
_isRefreshing = true;
try {
final filter = _getDateRangeFilter();
ref.refresh(filteredTransactionProvider(filter));
await Future.delayed(const Duration(milliseconds: 300));
} finally {
_isRefreshing = false;
}
}
void _updateDateUI(DateTime? from, DateTime? to) {
setState(() {
fromDate = from;
toDate = to;
fromDateController.text = from != null ? DateFormat('yyyy-MM-dd').format(from) : '';
toDateController.text = to != null ? DateFormat('yyyy-MM-dd').format(to) : '';
});
}
void _setDateRangeFromDropdown(String value) {
final now = DateTime.now();
switch (value) {
case 'today':
_updateDateUI(now, now);
break;
case 'yesterday':
final y = now.subtract(const Duration(days: 1));
_updateDateUI(y, y);
break;
case 'last_seven_days':
_updateDateUI(now.subtract(const Duration(days: 6)), now);
break;
case 'last_thirty_days':
_updateDateUI(now.subtract(const Duration(days: 29)), now);
break;
case 'current_month':
_updateDateUI(DateTime(now.year, now.month, 1), now);
break;
case 'last_month':
final first = DateTime(now.year, now.month - 1, 1);
final last = DateTime(now.year, now.month, 0);
_updateDateUI(first, last);
break;
case 'current_year':
_updateDateUI(DateTime(now.year, 1, 1), now);
break;
case 'custom_date':
// Dates stay as they are, user picks manually
break;
}
}
@override
void initState() {
super.initState();
final now = DateTime.now();
fromDate = now;
toDate = now;
fromDateController.text = DateFormat('yyyy-MM-dd').format(now);
toDateController.text = DateFormat('yyyy-MM-dd').format(now);
}
@override
void dispose() {
fromDateController.dispose();
toDateController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
final _lang = l.S.of(context);
return Consumer(
builder: (context, ref, __) {
final filter = _getDateRangeFilter();
final providerData = ref.watch(filteredTransactionProvider(filter));
final personalData = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
title: Text(_lang.dayBook),
backgroundColor: Colors.white,
elevation: 0.0,
iconTheme: const IconThemeData(color: Colors.black),
centerTitle: true,
actions: [
personalData.when(
data: (business) {
return providerData.when(
data: (transaction) {
return Row(
children: [
IconButton(
onPressed: () {
if (transaction.data?.isNotEmpty == true) {
// Using DayBook PDF generator
generateDayBookReportPdf(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showError(_lang.listIsEmpty);
}
},
icon: const HugeIcon(icon: HugeIcons.strokeRoundedPdf02, color: kSecondayColor),
),
// IconButton(
// padding: EdgeInsets.zero,
// onPressed: () {
// // Placeholder for Excel or other actions
// EasyLoading.showInfo('Excel export not implemented yet');
// },
// icon: SvgPicture.asset('assets/excel.svg'),
// ),
const 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: const Size.fromHeight(50),
child: Column(
children: [
Divider(thickness: 1, color: kBottomBorder, height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
flex: 2,
child: Row(
children: [
const Icon(IconlyLight.calendar, color: kPeraColor, size: 20),
const SizedBox(width: 3),
GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: true);
}
},
child: Text(
fromDate != null ? DateFormat('dd MMM yyyy').format(fromDate!) : _lang.from,
style: Theme.of(context).textTheme.bodyMedium,
),
),
const SizedBox(width: 4),
Text(_lang.to, style: _theme.textTheme.titleSmall),
const SizedBox(width: 4),
Flexible(
child: GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: false);
}
},
child: Text(
toDate != null ? DateFormat('dd MMM yyyy').format(toDate!) : _lang.to,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
],
),
),
const SizedBox(width: 2),
RotatedBox(
quarterTurns: 1,
child: Container(
height: 1,
width: 20,
color: kSubPeraColor,
),
),
const SizedBox(width: 2),
Expanded(
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
iconSize: 20,
value: selectedTime,
isExpanded: true,
items: dateOptions.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(
entry.value,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium,
),
);
}).toList(),
onChanged: (value) {
if (value == null) return;
setState(() {
selectedTime = value;
_showCustomDatePickers = value == 'custom_date';
});
if (value != 'custom_date') {
_setDateRangeFromDropdown(value);
_refreshFilteredProvider();
}
},
),
),
),
],
),
),
Divider(thickness: 1, color: kBottomBorder, height: 1),
],
),
),
),
body: RefreshIndicator(
onRefresh: _refreshFilteredProvider,
child: providerData.when(
data: (transactions) {
final allTransactions = transactions.data ?? [];
return Column(
children: [
// Overview Containers (Horizontal Scroll)
SizedBox.fromSize(
size: const Size.fromHeight(100),
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
scrollDirection: Axis.horizontal,
child: Row(
children: [
// Card 1: Total Sales
Container(
constraints: const BoxConstraints(minWidth: 170, maxHeight: 80),
decoration: BoxDecoration(
color: kPeraColor.withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(transactions.totalAmount ?? 0, addComma: true)}",
style: _theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(height: 4),
Text(
l.S.of(context).total,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
const SizedBox(width: 12),
// Card 2: Money In
Container(
constraints: const BoxConstraints(minWidth: 170, maxHeight: 80),
decoration: BoxDecoration(
color: DAppColors.kSuccess.withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(transactions.moneyIn ?? 0, addComma: true)}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
color: DAppColors.kSuccess,
),
),
const SizedBox(height: 4),
Text(
_lang.moneyIn,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
const SizedBox(width: 12),
// Card 3: Money Out
Container(
constraints: const BoxConstraints(minWidth: 170, maxHeight: 80),
decoration: BoxDecoration(
color: DAppColors.kError.withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(transactions.moneyOut ?? 0, addComma: true)}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
color: DAppColors.kError,
),
),
const SizedBox(height: 4),
Text(
_lang.moneyOut,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
],
),
),
),
// Tabs & List Data
Expanded(
child: DefaultTabController(
length: 2,
child: Builder(
builder: (tabContext) {
// Listen to tab changes to update list filtering
DefaultTabController.of(tabContext).addListener(() {
if (DefaultTabController.of(tabContext).indexIsChanging) {
// 0 = Credit (Money In), 1 = Debit (Money Out)
selectedTransactionTypeNotifier.value =
['credit', 'debit'][DefaultTabController.of(tabContext).index];
}
});
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox.fromSize(
size: const Size.fromHeight(40),
child: TabBar(
indicatorSize: TabBarIndicatorSize.tab,
unselectedLabelColor: Color(0xff4B5563),
tabs: [
Tab(text: _lang.moneyIn),
Tab(text: _lang.moneyOut),
],
),
),
Expanded(
child: ValueListenableBuilder<String>(
valueListenable: selectedTransactionTypeNotifier,
builder: (_, selectedTransactionType, __) {
// Filter transactions based on selected tab
final filteredList = allTransactions
.where((element) => element.type == selectedTransactionType)
.toList();
if (filteredList.isEmpty) {
return Center(child: Text(_lang.noTransactionFound));
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// Table Header
DefaultTextStyle.merge(
style: _theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 15,
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xffF7F7F7),
border: Border(bottom: Divider.createBorderSide(context)),
),
child: Row(
children: [
Expanded(
flex: 4,
child: Text(_lang.details, textAlign: TextAlign.start),
),
Expanded(
flex: 3,
child: Text(_lang.type, textAlign: TextAlign.center),
),
Expanded(
flex: 2,
child: Text(
selectedTransactionType == "credit"
? _lang.moneyIn
: _lang.moneyOut,
textAlign: TextAlign.end,
),
),
],
),
),
),
// List Items
Expanded(
child: ListView.builder(
itemCount: filteredList.length,
itemBuilder: (context, index) {
final t = filteredList[index];
// Using platform as name placeholder if party name is missing
// Adjust 't.user?.name' or 't.party?.name' based on your exact model
final displayTitle =
t.paymentType?.paymentType ?? t.platform ?? 'Unknown';
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
border: Border(
bottom: Divider.createBorderSide(context),
),
),
child: Row(
children: [
Expanded(
flex: 4,
child: Text.rich(
TextSpan(
text: "$displayTitle\n",
children: [
TextSpan(
text: t.date != null
? intl.DateFormat("dd MMM yyyy, hh:mm a")
.format(DateTime.parse(t.date!))
: "N/A",
style: const TextStyle(
fontWeight: FontWeight.normal,
color: Color(0xff4B5563),
fontSize: 12,
),
),
],
),
style: _theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
),
Expanded(
flex: 3,
child: Text(
t.platform?.capitalizeFirstLetter() ?? "N/A",
textAlign: TextAlign.center,
style: _theme.textTheme.bodySmall,
),
),
Expanded(
flex: 2,
child: Text(
"$currency${formatPointNumber(t.amount ?? 0, addComma: true)}",
textAlign: TextAlign.end,
style: TextStyle(
color: selectedTransactionType == 'credit'
? Colors.green
: Colors.red,
fontWeight: FontWeight.w600),
),
),
],
),
);
},
),
)
],
);
},
),
)
],
);
},
),
),
),
],
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text(e.toString())),
),
),
),
);
},
);
}
}
// Helper extension if not already in your project
extension StringExtension on String {
String capitalizeFirstLetter() {
if (this.isEmpty) return this;
return "${this[0].toUpperCase()}${this.substring(1).toLowerCase()}";
}
}

View File

@@ -0,0 +1,917 @@
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:iconly/iconly.dart';
import 'package:intl/intl.dart';
import 'package:mobile_pos/generated/l10n.dart' as l;
import 'package:mobile_pos/pdf_report/due_report/due_report_excel.dart';
import 'package:mobile_pos/pdf_report/due_report/due_report_pdf.dart';
import 'package:nb_utils/nb_utils.dart';
import '../../../GlobalComponents/glonal_popup.dart';
import '../../../PDF Invoice/due_invoice_pdf.dart';
import '../../../Provider/profile_provider.dart';
import '../../../Provider/transactions_provider.dart';
import '../../../constant.dart';
import '../../../core/theme/_app_colors.dart';
import '../../../currency.dart';
import '../../../thermal priting invoices/model/print_transaction_model.dart';
import '../../../thermal priting invoices/provider/print_thermal_invoice_provider.dart';
import '../../../widgets/empty_widget/_empty_widget.dart';
import '../../Due Calculation/Providers/due_provider.dart';
import '../../../service/check_user_role_permission_provider.dart';
import '../../invoice_details/due_invoice_details.dart';
class DueReportScreen extends ConsumerStatefulWidget {
const DueReportScreen({super.key});
@override
// ignore: library_private_types_in_public_api
_DueReportScreenState createState() => _DueReportScreenState();
}
class _DueReportScreenState extends ConsumerState<DueReportScreen> {
final TextEditingController fromDateController = TextEditingController();
final TextEditingController toDateController = TextEditingController();
final Map<String, String> dateOptions = {
'today': l.S.current.today,
'yesterday': l.S.current.yesterday,
'last_seven_days': l.S.current.last7Days,
'last_thirty_days': l.S.current.last30Days,
'current_month': l.S.current.currentMonth,
'last_month': l.S.current.lastMonth,
'current_year': l.S.current.currentYear,
'custom_date': l.S.current.customerDate,
};
String selectedTime = 'today';
bool _isRefreshing = false;
bool _showCustomDatePickers = false;
DateTime? fromDate;
DateTime? toDate;
String searchCustomer = '';
/// Generates the date range string for the provider
FilterModel _getDateRangeFilter() {
if (_showCustomDatePickers && fromDate != null && toDate != null) {
return FilterModel(
duration: 'custom_date',
fromDate: DateFormat('yyyy-MM-dd', 'en_US').format(fromDate!),
toDate: DateFormat('yyyy-MM-dd', 'en_US').format(toDate!),
);
} else {
return FilterModel(duration: selectedTime.toLowerCase());
}
}
Future<void> _selectDate({
required BuildContext context,
required bool isFrom,
}) async {
final DateTime? picked = await showDatePicker(
context: context,
firstDate: DateTime(2021),
lastDate: DateTime.now(),
initialDate: isFrom ? fromDate ?? DateTime.now() : toDate ?? DateTime.now(),
);
if (picked != null) {
setState(() {
if (isFrom) {
fromDate = picked;
fromDateController.text = DateFormat('yyyy-MM-dd').format(picked);
} else {
toDate = picked;
toDateController.text = DateFormat('yyyy-MM-dd').format(picked);
}
});
if (fromDate != null && toDate != null) _refreshFilteredProvider();
}
}
Future<void> _refreshFilteredProvider() async {
if (_isRefreshing) return;
_isRefreshing = true;
try {
final filter = _getDateRangeFilter();
ref.refresh(filteredDueProvider(filter));
await Future.delayed(const Duration(milliseconds: 300)); // small delay
} finally {
_isRefreshing = false;
}
}
@override
void dispose() {
fromDateController.dispose();
toDateController.dispose();
super.dispose();
}
void _updateDateUI(DateTime? from, DateTime? to) {
setState(() {
fromDate = from;
toDate = to;
fromDateController.text = from != null ? DateFormat('yyyy-MM-dd').format(from) : '';
toDateController.text = to != null ? DateFormat('yyyy-MM-dd').format(to) : '';
});
}
void _setDateRangeFromDropdown(String value) {
final now = DateTime.now();
switch (value) {
case 'today':
_updateDateUI(now, now);
break;
case 'yesterday':
final y = now.subtract(const Duration(days: 1));
_updateDateUI(y, y);
break;
case 'last_seven_days':
_updateDateUI(
now.subtract(const Duration(days: 6)),
now,
);
break;
case 'last_thirty_days':
_updateDateUI(
now.subtract(const Duration(days: 29)),
now,
);
break;
case 'current_month':
_updateDateUI(
DateTime(now.year, now.month, 1),
now,
);
break;
case 'last_month':
final first = DateTime(now.year, now.month - 1, 1);
final last = DateTime(now.year, now.month, 0);
_updateDateUI(first, last);
break;
case 'current_year':
_updateDateUI(
DateTime(now.year, 1, 1),
now,
);
break;
case 'custom_date':
// Custom: User will select manually
_updateDateUI(null, null);
break;
}
}
@override
void initState() {
super.initState();
final now = DateTime.now();
// Set initial From and To date = TODAY
fromDate = now;
toDate = now;
fromDateController.text = DateFormat('yyyy-MM-dd').format(now);
toDateController.text = DateFormat('yyyy-MM-dd').format(now);
}
@override
Widget build(BuildContext context) {
// final translateTime = getTranslateTime(context);
final _theme = Theme.of(context);
final _lang = l.S.of(context);
return Consumer(
builder: (context, ref, __) {
final providerData = ref.watch(filteredDueProvider(_getDateRangeFilter()));
final printerData = ref.watch(thermalPrinterProvider);
final personalData = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
title: Text(
l.S.of(context).dueReport,
),
actions: [
personalData.when(
data: (business) {
return providerData.when(
data: (transaction) {
return Row(
children: [
IconButton(
onPressed: () {
if (transaction.isNotEmpty) {
generateDueReportPdf(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showError(_lang.listIsEmpty);
}
},
icon: HugeIcon(icon: HugeIcons.strokeRoundedPdf02, color: kSecondayColor),
),
IconButton(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
onPressed: () {
if (transaction.isNotEmpty) {
generateDueReportExcel(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showInfo(_lang.noDataAvailableForGeneratePdf);
}
},
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: const Size.fromHeight(50),
child: Column(
children: [
Divider(thickness: 1, color: kBottomBorder, height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
flex: 2,
child: Row(
children: [
Icon(IconlyLight.calendar, color: kPeraColor, size: 20),
SizedBox(width: 3),
GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: true);
}
},
child: Text(
fromDate != null ? DateFormat('dd MMM yyyy').format(fromDate!) : _lang.from,
style: Theme.of(context).textTheme.bodyMedium,
),
),
SizedBox(width: 4),
Text(
_lang.to,
style: _theme.textTheme.titleSmall,
),
SizedBox(width: 4),
Flexible(
child: GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: false);
}
},
child: Text(
toDate != null ? DateFormat('dd MMM yyyy').format(toDate!) : _lang.to,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
],
),
),
SizedBox(width: 2),
RotatedBox(
quarterTurns: 1,
child: Container(
height: 1,
width: 20,
color: kSubPeraColor,
),
),
SizedBox(width: 2),
Expanded(
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
iconSize: 20,
value: selectedTime,
isExpanded: true,
items: dateOptions.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(
entry.value,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium,
),
);
}).toList(),
onChanged: (value) {
if (value == null) return;
setState(() {
selectedTime = value;
_showCustomDatePickers = value == 'custom_date';
});
if (value != 'custom_date') {
_setDateRangeFromDropdown(value);
_refreshFilteredProvider();
}
},
),
),
),
],
),
),
Divider(thickness: 1, color: kBottomBorder, height: 1),
],
),
),
iconTheme: const IconThemeData(color: Colors.black),
centerTitle: true,
backgroundColor: Colors.white,
elevation: 0.0,
),
body: RefreshIndicator(
onRefresh: () => _refreshFilteredProvider(),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children: [
if (permissionService.hasPermission(Permit.dueReportsRead.value)) ...{
Padding(
padding: const EdgeInsets.only(right: 16.0, left: 16.0, top: 12, bottom: 0),
child: Column(
children: [
TextFormField(
onChanged: (value) {
setState(() {
searchCustomer = value.toLowerCase().trim();
});
},
decoration: InputDecoration(
prefixIconConstraints: const BoxConstraints(
minHeight: 20,
minWidth: 20,
),
prefixIcon: Padding(
padding: const EdgeInsetsDirectional.only(start: 10),
child: Icon(
FeatherIcons.search,
color: kGrey6,
),
),
hintText: l.S.of(context).searchH,
),
),
],
),
),
providerData.when(data: (transaction) {
final filteredTransactions = transaction.where((due) {
final customerName = due.user?.name?.toLowerCase() ?? '';
final invoiceNumber = due.invoiceNumber?.toLowerCase() ?? '';
return customerName.contains(searchCustomer) || invoiceNumber.contains(searchCustomer);
}).toList();
double totalReceiveDue = 0; // Customer receive
double totalPaidDue = 0; // Supplier paid
for (var element in filteredTransactions) {
final amount = element.payDueAmount ?? 0;
if (element.party?.type == 'Supplier') {
totalPaidDue += amount; // For Suppliers
} else {
totalReceiveDue += amount; // For Customers
}
}
return filteredTransactions.isNotEmpty
? Column(
children: [
Padding(
padding: EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Row(
children: [
Expanded(
child: Container(
height: 77,
decoration: BoxDecoration(
color: kSuccessColor.withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(totalReceiveDue)}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
l.S.of(context).customerPay,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
),
SizedBox(width: 12),
Expanded(
child: Container(
height: 77,
width: double.infinity,
decoration: BoxDecoration(
color: DAppColors.kWarning.withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(totalPaidDue)}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
l.S.of(context).supplerPay,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
),
],
),
),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: filteredTransactions.length,
itemBuilder: (context, index) {
final item = transaction[index];
final partyName = item.party?.name ?? 'n/a';
final partyType = item.party?.type ?? 'n/a';
final invoiceNo = item.invoiceNumber ?? 'n/a';
final dueAmount = item.dueAmountAfterPay ?? 0;
final totalDue = item.totalDue ?? 0;
final paidAmount = (totalDue - dueAmount).clamp(0, double.infinity);
// ---- SAFE DATE ----
DateTime? paymentDate;
try {
if (item.paymentDate != null && item.paymentDate!.isNotEmpty) {
paymentDate = DateTime.parse(item.paymentDate!);
}
} catch (_) {
paymentDate = null;
}
return GestureDetector(
onTap: () {
if (personalData.value != null) {
DueInvoiceDetails(
dueCollection: filteredTransactions[index],
personalInformationModel: personalData.value!,
).launch(context);
}
},
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
width: context.width(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ------------------- TOP ROW ----------------------
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(partyName,
style: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 15,
)),
const SizedBox(width: 10),
if (partyType == 'Supplier')
Text(
'[S]',
style: _theme.textTheme.titleSmall
?.copyWith(color: kMainColor),
),
],
),
Text('#$invoiceNo'),
],
),
const SizedBox(height: 6),
// ------------------- STATUS + DATE ----------------------
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: dueAmount <= 0
? const Color(0xff0dbf7d).withValues(alpha: 0.1)
: const Color(0xFFED1A3B).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
dueAmount <= 0
? l.S.of(context).fullyPaid
: l.S.of(context).stillUnpaid,
style: _theme.textTheme.titleSmall?.copyWith(
color: dueAmount <= 0
? const Color(0xff0dbf7d)
: const Color(0xFFED1A3B),
),
),
),
Text(
paymentDate == null
? '--'
: DateFormat.yMMMd().format(paymentDate),
style: _theme.textTheme.bodyMedium
?.copyWith(color: kPeragrapColor),
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${l.S.of(context).total} : $currency${formatPointNumber(totalDue)}',
style: _theme.textTheme.titleSmall?.copyWith(
color: kPeraColor,
),
),
Text(
'${l.S.of(context).paid} : $currency${formatPointNumber(paidAmount)}',
style: _theme.textTheme.titleSmall?.copyWith(
color: kPeraColor,
),
),
],
),
SizedBox(height: 3),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// if (dueAmount > 0)
Text(
'${l.S.of(context).due}: $currency${formatPointNumber(dueAmount)}',
style: _theme.textTheme.titleMedium?.copyWith(
color: kPeraColor,
),
),
// ------------------- PERSONAL DATA ----------------------
personalData.when(
data: (data) {
return Row(
children: [
IconButton(
padding: EdgeInsets.zero,
visualDensity:
const VisualDensity(horizontal: -4, vertical: -4),
onPressed: () async {
if (Theme.of(context).platform ==
TargetPlatform.android) {
final model = PrintDueTransactionModel(
dueTransactionModel: item,
personalInformationModel: data,
);
await printerData.printDueThermalInvoiceNow(
transaction: model,
invoiceSize: data.data?.invoiceSize,
context: context,
);
}
},
icon: const Icon(FeatherIcons.printer,
color: kPeraColor, size: 22),
),
Row(
children: [
IconButton(
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(
horizontal: -4, vertical: -4),
onPressed: () => DueInvoicePDF.generateDueDocument(
item,
data,
context,
showPreview: true,
),
icon: HugeIcon(
icon: HugeIcons.strokeRoundedPdf02,
size: 22,
color: kPeraColor,
),
),
IconButton(
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(
horizontal: -4, vertical: -4),
onPressed: () => DueInvoicePDF.generateDueDocument(
item,
data,
context,
download: true,
),
icon: HugeIcon(
icon: HugeIcons.strokeRoundedDownload01,
size: 22,
color: kPeraColor,
),
),
IconButton(
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(
horizontal: -4, vertical: -4),
onPressed: () => DueInvoicePDF.generateDueDocument(
item,
data,
context,
isShare: true,
),
icon: HugeIcon(
icon: HugeIcons.strokeRoundedShare08,
size: 22,
color: kPeraColor,
),
),
],
),
],
);
},
error: (e, stack) => Text(e.toString()),
loading: () => Text(l.S.of(context).loading),
),
],
),
],
),
),
// Divider
Container(
height: 1,
color: kBottomBorder,
),
],
),
);
},
// itemBuilder: (context, index) {
// return GestureDetector(
// onTap: () {
// DueInvoiceDetails(
// dueCollection: filteredTransactions[index],
// personalInformationModel: personalData.value!,
// ).launch(context);
// },
// child: Column(
// children: [
// Container(
// padding: const EdgeInsets.all(20),
// width: context.width(),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Row(
// children: [
// Text(
// transaction[index].party?.name ?? '',
// style: const TextStyle(fontSize: 16),
// ),
// const SizedBox(
// width: 10,
// ),
// Visibility(
// visible: transaction[index].party?.type == 'Supplier',
// child: const Text(
// '[S]',
// style: TextStyle(
// //fontSize: 16,
// color: kMainColor),
// ),
// )
// ],
// ),
// Text('#${transaction[index].invoiceNumber}'),
// ],
// ),
// const SizedBox(height: 10),
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Container(
// padding: const EdgeInsets.all(8),
// decoration: BoxDecoration(
// color: transaction[index].dueAmountAfterPay! <= 0
// ? const Color(0xff0dbf7d).withOpacity(0.1)
// : const Color(0xFFED1A3B).withOpacity(0.1),
// borderRadius: const BorderRadius.all(Radius.circular(10))),
// child: Text(
// transaction[index].dueAmountAfterPay! <= 0 ? lang.S.of(context).fullyPaid : lang.S.of(context).stillUnpaid,
// style: TextStyle(color: transaction[index].dueAmountAfterPay! <= 0 ? const Color(0xff0dbf7d) : const Color(0xFFED1A3B)),
// ),
// ),
// Text(
// DateFormat.yMMMd().format(DateTime.parse(transaction[index].paymentDate ?? '')),
// style: const TextStyle(color: Colors.grey),
// ),
// ],
// ),
// const SizedBox(height: 10),
// Text(
// '${lang.S.of(context).total} : $currency${transaction[index].totalDue?.toStringAsFixed(2) ?? '0'}',
// style: const TextStyle(color: Colors.grey),
// ),
// const SizedBox(height: 10),
// Text(
// '${lang.S.of(context).paid} : $currency ${(transaction[index].totalDue!.toDouble() - transaction[index].dueAmountAfterPay!.toDouble()).toStringAsFixed(2) ?? '/a'}',
// style: const TextStyle(color: Colors.grey),
// ),
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Text(
// '${lang.S.of(context).due}: $currency ${transaction[index].dueAmountAfterPay?.toStringAsFixed(2)}',
// style: const TextStyle(fontSize: 16),
// ).visible((transaction[index].dueAmountAfterPay ?? 0) > 0),
// personalData.when(data: (data) {
// return Row(
// children: [
// IconButton(
// padding: EdgeInsets.zero,
// visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
// onPressed: () async {
// if ((Theme.of(context).platform == TargetPlatform.android)) {
// ///________Print_______________________________________________________
//
// PrintDueTransactionModel model =
// PrintDueTransactionModel(dueTransactionModel: transaction[index], personalInformationModel: data);
// await printerData.printDueThermalInvoiceNow(
// transaction: model, invoiceSize: data.data?.invoiceSize, context: context);
// }
// },
// icon: const Icon(
// FeatherIcons.printer,
// color: Colors.grey,
// size: 22,
// )),
// const SizedBox(width: 10),
// businessSettingData.when(data: (business) {
// return Row(
// children: [
// IconButton(
// padding: EdgeInsets.zero,
// visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
// onPressed: () =>
// DueInvoicePDF.generateDueDocument(transaction[index], data, context, business, showPreview: true),
// icon: const Icon(
// Icons.picture_as_pdf,
// color: Colors.grey,
// size: 22,
// )),
// IconButton(
// padding: EdgeInsets.zero,
// visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
// onPressed: () => DueInvoicePDF.generateDueDocument(transaction[index], data, context, business, download: true),
// icon: const Icon(
// FeatherIcons.download,
// color: Colors.grey,
// size: 22,
// )),
// IconButton(
// padding: EdgeInsets.zero,
// visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
// onPressed: () => DueInvoicePDF.generateDueDocument(transaction[index], data, context, business, isShare: true),
// icon: const Icon(
// Icons.share,
// color: Colors.grey,
// size: 22,
// )),
// ],
// );
// }, error: (e, stack) {
// return Text(e.toString());
// }, loading: () {
// return const Center(
// child: CircularProgressIndicator(),
// );
// })
// ],
// );
// }, error: (e, stack) {
// return Text(e.toString());
// }, loading: () {
// //return const Text('Loading');
// return Text(lang.S.of(context).loading);
// }),
// ],
// ),
// ],
// ),
// ),
// Container(
// height: 0.5,
// width: context.width(),
// color: Colors.grey,
// )
// ],
// ),
// );
// },
)
],
)
: Center(
child: Text(
l.S.of(context).collectDues,
maxLines: 2,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.bold, fontSize: 20.0),
),
);
}, error: (e, stack) {
return Text(e.toString());
}, loading: () {
return const Center(child: CircularProgressIndicator());
}),
} else
Center(child: PermitDenyWidget()),
],
),
),
),
),
);
},
);
}
}

View File

@@ -0,0 +1,587 @@
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:iconly/iconly.dart';
import 'package:intl/intl.dart';
import 'package:mobile_pos/Screens/Expense/Providers/all_expanse_provider.dart';
import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/currency.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 '../../../Provider/transactions_provider.dart';
import '../../../global_report_filter_bottomshet.dart';
import '../../../pdf_report/expense_report/expense_report_excel.dart';
import '../../../pdf_report/expense_report/expense_report_pdf.dart';
import '../../../service/check_actions_when_no_branch.dart';
import '../../../thermal priting invoices/provider/print_thermal_invoice_provider.dart';
import '../../../widgets/empty_widget/_empty_widget.dart';
import '../../../service/check_user_role_permission_provider.dart';
import '../../Expense/add_erxpense.dart';
class ExpenseReport extends ConsumerStatefulWidget {
const ExpenseReport({super.key, this.isFromExpense});
final bool? isFromExpense;
@override
ConsumerState<ExpenseReport> createState() => _ExpenseReportState();
}
class _ExpenseReportState extends ConsumerState<ExpenseReport> {
final TextEditingController fromDateController = TextEditingController();
final TextEditingController toDateController = TextEditingController();
final Map<String, String> dateOptions = {
'today': l.S.current.today,
'yesterday': l.S.current.yesterday,
'last_seven_days': l.S.current.last7Days,
'last_thirty_days': l.S.current.last30Days,
'current_month': l.S.current.currentMonth,
'last_month': l.S.current.lastMonth,
'current_year': l.S.current.currentYear,
'custom_date': l.S.current.customerDate,
};
String selectedTime = 'today';
bool _isRefreshing = false;
bool _showCustomDatePickers = false;
DateTime? fromDate;
DateTime? toDate;
String searchCustomer = '';
/// Generates the date range string for the provider
FilterModel _getDateRangeFilter() {
if (_showCustomDatePickers && fromDate != null && toDate != null) {
return FilterModel(
duration: 'custom_date',
fromDate: DateFormat('yyyy-MM-dd', 'en_US').format(fromDate!),
toDate: DateFormat('yyyy-MM-dd', 'en_US').format(toDate!),
);
} else {
return FilterModel(duration: selectedTime.toLowerCase());
}
}
Future<void> _selectDate({
required BuildContext context,
required bool isFrom,
}) async {
final DateTime? picked = await showDatePicker(
context: context,
firstDate: DateTime(2021),
lastDate: DateTime.now(),
initialDate: isFrom ? fromDate ?? DateTime.now() : toDate ?? DateTime.now(),
);
if (picked != null) {
setState(() {
if (isFrom) {
fromDate = picked;
fromDateController.text = DateFormat('yyyy-MM-dd').format(picked);
} else {
toDate = picked;
toDateController.text = DateFormat('yyyy-MM-dd').format(picked);
}
});
if (fromDate != null && toDate != null) _refreshFilteredProvider();
}
}
Future<void> _refreshFilteredProvider() async {
if (_isRefreshing) return;
_isRefreshing = true;
try {
final filter = _getDateRangeFilter();
ref.refresh(filteredSaleProvider(filter));
await Future.delayed(const Duration(milliseconds: 300)); // small delay
} finally {
_isRefreshing = false;
}
}
@override
void dispose() {
fromDateController.dispose();
toDateController.dispose();
super.dispose();
}
void _updateDateUI(DateTime? from, DateTime? to) {
setState(() {
fromDate = from;
toDate = to;
fromDateController.text = from != null ? DateFormat('yyyy-MM-dd').format(from) : '';
toDateController.text = to != null ? DateFormat('yyyy-MM-dd').format(to) : '';
});
}
void _setDateRangeFromDropdown(String value) {
final now = DateTime.now();
switch (value) {
case 'today':
_updateDateUI(now, now);
break;
case 'yesterday':
final y = now.subtract(const Duration(days: 1));
_updateDateUI(y, y);
break;
case 'last_seven_days':
_updateDateUI(
now.subtract(const Duration(days: 6)),
now,
);
break;
case 'last_thirty_days':
_updateDateUI(
now.subtract(const Duration(days: 29)),
now,
);
break;
case 'current_month':
_updateDateUI(
DateTime(now.year, now.month, 1),
now,
);
break;
case 'last_month':
final first = DateTime(now.year, now.month - 1, 1);
final last = DateTime(now.year, now.month, 0);
_updateDateUI(first, last);
break;
case 'current_year':
_updateDateUI(
DateTime(now.year, 1, 1),
now,
);
break;
case 'custom_date':
// Custom: User will select manually
_updateDateUI(null, null);
break;
}
}
@override
void initState() {
super.initState();
final now = DateTime.now();
// Set initial From and To date = TODAY
fromDate = now;
toDate = now;
fromDateController.text = DateFormat('yyyy-MM-dd').format(now);
toDateController.text = DateFormat('yyyy-MM-dd').format(now);
}
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
final _lang = l.S.of(context);
return Consumer(builder: (context, ref, __) {
final expenseData = ref.watch(filteredExpenseProvider(_getDateRangeFilter()));
final personalData = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
return personalData.when(
data: (business) {
return GlobalPopup(
child: expenseData.when(
data: (allExpense) {
final filteredExpense = allExpense.where((expenses) {
final expenseFor = expenses.expanseFor?.toLowerCase() ?? '';
return expenseFor.contains(searchCustomer);
}).toList();
final totalExpense = filteredExpense.fold<num>(0, (sum, income) => sum + (income.amount ?? 0));
return Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
title: Text(l.S.of(context).expenseReport),
iconTheme: const IconThemeData(color: Colors.black),
centerTitle: true,
backgroundColor: Colors.white,
elevation: 0.0,
actions: [
IconButton(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
onPressed: () {
if (allExpense.isNotEmpty) {
generateExpenseReportPdf(context, allExpense, business, fromDate, toDate, selectedTime);
} else {
EasyLoading.showInfo(l.S.of(context).noDataAvailableForGeneratePdf);
}
},
icon: HugeIcon(
icon: HugeIcons.strokeRoundedPdf01,
color: kSecondayColor,
),
),
IconButton(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
onPressed: () {
if (allExpense.isNotEmpty) {
generateExpenseReportExcel(context, allExpense, business, fromDate, toDate, selectedTime);
} else {
EasyLoading.showInfo(l.S.of(context).noDataAvailableForGeneratePdf);
}
},
icon: SvgPicture.asset('assets/excel.svg'),
),
SizedBox(width: 8),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(50),
child: Column(
children: [
Divider(thickness: 1, color: kBottomBorder, height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
flex: 2,
child: Row(
children: [
Icon(IconlyLight.calendar, color: kPeraColor, size: 20),
SizedBox(width: 3),
GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: true);
}
},
child: Text(
fromDate != null ? DateFormat('dd MMM yyyy').format(fromDate!) : _lang.from,
style: Theme.of(context).textTheme.bodyMedium,
),
),
SizedBox(width: 4),
Text(
_lang.to,
style: _theme.textTheme.titleSmall,
),
SizedBox(width: 4),
Flexible(
child: GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: false);
}
},
child: Text(
toDate != null ? DateFormat('dd MMM yyyy').format(toDate!) : _lang.to,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
],
),
),
SizedBox(width: 2),
RotatedBox(
quarterTurns: 1,
child: Container(
height: 1,
width: 20,
color: kSubPeraColor,
),
),
SizedBox(width: 2),
Expanded(
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
iconSize: 20,
value: selectedTime,
isExpanded: true,
items: dateOptions.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(
entry.value,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium,
),
);
}).toList(),
onChanged: (value) {
if (value == null) return;
setState(() {
selectedTime = value;
_showCustomDatePickers = value == 'custom_date';
});
if (value != 'custom_date') {
_setDateRangeFromDropdown(value);
_refreshFilteredProvider();
}
},
),
),
),
],
),
),
Divider(thickness: 1, color: kBottomBorder, height: 1),
],
),
),
),
body: SingleChildScrollView(
padding: EdgeInsetsDirectional.symmetric(vertical: 16),
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children: [
///__________expense_data_table____________________________________________
if (permissionService.hasPermission(Permit.expenseReportsRead.value)) ...{
Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: TextFormField(
decoration: InputDecoration(
hintText: l.S.of(context).searchWith,
),
onChanged: (value) => setState(() {
searchCustomer = value.toLowerCase().trim();
}),
),
),
SizedBox(height: 10),
Container(
width: context.width(),
padding: EdgeInsetsDirectional.symmetric(vertical: 13, horizontal: 24),
height: 50,
decoration: const BoxDecoration(color: kMainColor50),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
l.S.of(context).expenseFor,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.start,
),
),
Expanded(
child: Text(
l.S.of(context).date,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
),
Expanded(
child: Text(
l.S.of(context).amount,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.end,
),
)
],
),
),
],
),
if (filteredExpense.isEmpty)
Padding(
padding: const EdgeInsets.all(20),
child: Center(
child: Text(l.S.of(context).noData),
),
)
else
SizedBox(
width: context.width(),
child: ListView.builder(
shrinkWrap: true,
itemCount: filteredExpense.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
final expense = filteredExpense[index];
return Column(
children: [
Padding(
padding: EdgeInsetsDirectional.symmetric(vertical: 10, horizontal: 24),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
expense.expanseFor ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
expense.category?.categoryName ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodySmall?.copyWith(
color: kPeraColor,
),
),
],
),
),
Expanded(
child: Text(
DateTime.tryParse(expense.expenseDate ?? '') != null
? DateFormat.yMMMd()
.format(DateTime.parse(expense.expenseDate ?? ''))
: '',
textAlign: TextAlign.center,
),
),
Expanded(
child: Text(
'$currency${expense.amount?.toStringAsFixed(2)}',
textAlign: TextAlign.end,
),
)
],
),
),
Container(
height: 1,
color: Colors.black12,
)
],
);
},
),
),
} else
Center(child: PermitDenyWidget()),
],
),
),
bottomNavigationBar: widget.isFromExpense == true
? Visibility(
visible: permissionService.hasPermission(Permit.expenseReportsRead.value),
child: Container(
height: 50,
padding: const EdgeInsets.all(10),
decoration: const BoxDecoration(color: kMainColor50),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${l.S.of(context).total}:',
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
Text(
'$currency${totalExpense.toStringAsFixed(2)}',
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
)
],
),
),
)
: Padding(
padding: const EdgeInsets.all(16.0),
child: SizedBox(
height: 120,
child: Column(
children: [
Visibility(
visible: permissionService.hasPermission(Permit.expenseReportsRead.value),
child: Container(
height: 50,
padding: const EdgeInsets.all(10),
decoration: const BoxDecoration(color: kMainColor50),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${l.S.of(context).total}:',
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
Text(
'$currency${totalExpense.toStringAsFixed(2)}',
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
)
],
),
),
),
SizedBox(height: 16),
personalData.when(data: (details) {
return ElevatedButton(
onPressed: () async {
bool result = await checkActionWhenNoBranch(ref: ref, context: context);
if (!result) {
return;
}
bool result2 = await const AddExpense().launch(context);
if (result2) {
await _refreshFilteredProvider();
}
},
child: Text(l.S.of(context).addExpense),
);
}, error: (e, stack) {
return Text(e.toString());
}, loading: () {
return const Center(
child: CircularProgressIndicator(),
);
})
],
),
),
),
);
},
error: (error, stackTrace) => Center(child: Text(error.toString())),
loading: () => const Center(child: CircularProgressIndicator()),
),
);
},
error: (error, stackTrace) => Center(child: Text(error.toString())),
loading: () => const Center(child: CircularProgressIndicator()),
);
});
}
}

View File

@@ -0,0 +1,755 @@
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/Screens/Products/Model/product_model.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import 'package:mobile_pos/pdf_report/expire_report/expire_report_pdf.dart';
import '../../../Provider/product_provider.dart';
import '../../../constant.dart';
import '../../../currency.dart';
import '../../../http_client/custome_http_client.dart';
import '../../../widgets/empty_widget/_empty_widget.dart';
import '../../../service/check_user_role_permission_provider.dart';
class ExpiredList extends StatefulWidget {
const ExpiredList({super.key});
@override
ExpiredListState createState() => ExpiredListState();
}
class ExpiredListState extends State<ExpiredList> {
String productSearch = '';
bool _isRefreshing = false;
String selectedFilter = 'All';
final TextEditingController searchController = TextEditingController();
final TextEditingController fromDateController = TextEditingController();
final TextEditingController toDateController = TextEditingController();
String? selectedCategory;
DateTime? _fromDate;
DateTime? _toDate;
String? _errorMessage;
bool _isFiltered = false;
DateTime? _firstExpenseDate;
@override
void dispose() {
searchController.dispose();
fromDateController.dispose();
toDateController.dispose();
super.dispose();
}
Future<void> refreshData(WidgetRef ref) async {
if (_isRefreshing) return;
_isRefreshing = true;
ref.refresh(productProvider);
await Future.delayed(const Duration(seconds: 1));
_isRefreshing = false;
}
String _formatDate(DateTime date) {
return DateFormat('yyyy-MM-dd').format(date);
}
Future<DateTime?> _selectDate(BuildContext context, {DateTime? initialDate}) async {
return await showDatePicker(
context: context,
initialDate: initialDate ?? DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2101),
);
}
Future<void> _selectFromDate(BuildContext context) async {
DateTime? selectedDate = await _selectDate(context);
if (selectedDate != null) {
setState(() {
_fromDate = selectedDate;
fromDateController.text = _formatDate(selectedDate);
_errorMessage = null;
});
}
}
Future<void> _selectToDate(BuildContext context) async {
DateTime? selectedDate = await _selectDate(context, initialDate: _fromDate ?? DateTime.now());
if (selectedDate != null) {
if (_fromDate != null && selectedDate.isBefore(_fromDate!)) {
setState(() {
_errorMessage = lang.S.of(context).dateFilterWarn;
});
} else {
setState(() {
_toDate = selectedDate;
toDateController.text = _formatDate(selectedDate);
_errorMessage = null;
});
}
}
}
void _clearFilters() {
setState(() {
selectedCategory = null;
selectedFilter = 'All';
_fromDate = null;
_toDate = null;
fromDateController.clear();
toDateController.clear();
_errorMessage = null;
_isFiltered = false;
});
Navigator.pop(context);
}
void _applyFilters() {
if (_fromDate != null && _toDate != null && _toDate!.isBefore(_fromDate!)) {
setState(() {
_errorMessage = lang.S.of(context).dateFilterWarn;
});
return;
}
setState(() {
_isFiltered = true;
_errorMessage = null;
});
Navigator.pop(context);
}
List<Product> _filterProducts(List<Product> products) {
return products
.map((product) {
// Filter stocks of the product
final filteredStocks = product.stocks?.where((stock) {
final stockExpireDate = stock.expireDate != null ? DateTime.tryParse(stock.expireDate!) : null;
if (stockExpireDate == null) return false;
final now = DateTime.now();
// Custom date filter
if (selectedFilter == 'Custom') {
if (_fromDate != null && stockExpireDate.isBefore(_fromDate!)) return false;
if (_toDate != null && stockExpireDate.isAfter(_toDate!)) return false;
return true;
}
// Expiration status filters
switch (selectedFilter) {
case 'All':
return true;
case 'Expired':
final daysUntilExpiration = stockExpireDate.difference(now).inDays;
return daysUntilExpiration < 0; // expired if in the past
case '7 days':
final daysUntilExpiration = stockExpireDate.difference(now).inDays;
return daysUntilExpiration >= 0 && daysUntilExpiration <= 7;
case '15 days':
final daysUntilExpiration = stockExpireDate.difference(now).inDays;
return daysUntilExpiration > 7 && daysUntilExpiration <= 15;
case '30 days':
final daysUntilExpiration = stockExpireDate.difference(now).inDays;
return daysUntilExpiration > 15 && daysUntilExpiration <= 30;
}
return true;
}).toList();
if (filteredStocks == null || filteredStocks.isEmpty) return null;
// Return product with filtered stocks
return Product(
id: product.id,
productName: product.productName,
productCode: product.productCode,
productPurchasePrice: product.productPurchasePrice,
productSalePrice: product.productSalePrice,
stocks: filteredStocks,
category: product.category,
);
})
.whereType<Product>()
.toList();
}
double calculateStockValue(List<Product> products) {
double total = 0;
for (final product in products) {
if (product.stocks != null) {
for (final stock in product.stocks!) {
final qty = stock.productStock ?? 0;
final price = stock.productPurchasePrice ?? product.productPurchasePrice ?? 0;
total += qty * price;
}
}
}
return total;
}
@override
Widget build(BuildContext context) {
final _lang = lang.S.of(context);
final _theme = Theme.of(context);
return Consumer(builder: (context, ref, __) {
final providerData = ref.watch(productProvider);
final personalInfoProvider = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
return personalInfoProvider.when(
data: (business) {
return providerData.when(
data: (products) {
if (_firstExpenseDate == null && products.isNotEmpty) {
// Find the earliest expiration date across all stocks
DateTime? earliestDate;
for (final product in products) {
if (product.stocks != null) {
for (final stock in product.stocks!) {
if (stock.expireDate != null) {
final date = DateTime.tryParse(stock.expireDate!);
if (date != null && (earliestDate == null || date.isBefore(earliestDate))) {
earliestDate = date;
}
}
}
}
}
_firstExpenseDate = earliestDate;
}
final filteredProducts = _filterProducts(products);
final totalParPrice = calculateStockValue(filteredProducts);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text(_lang.expiredList),
iconTheme: const IconThemeData(color: Colors.black),
centerTitle: true,
backgroundColor: Colors.white,
elevation: 0.0,
actions: [
IconButton(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
onPressed: () {
if (!permissionService.hasPermission(Permit.expiredProductReportsRead.value)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text(lang.S.of(context).createPdfWarn),
),
);
return;
}
if (filteredProducts.isNotEmpty) {
generateExpireReportPdf(
context, filteredProducts, business, _firstExpenseDate, DateTime.now());
} else {
EasyLoading.showInfo(lang.S.of(context).genPdfWarn);
}
},
icon: HugeIcon(
icon: HugeIcons.strokeRoundedPdf01,
color: kSecondayColor,
),
),
const SizedBox(width: 8),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(60),
child: Column(
children: [
Divider(thickness: 1, color: kBottomBorder),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
child: TextFormField(
controller: searchController,
decoration: InputDecoration(
hintText: lang.S.of(context).searchWith,
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (searchController.text.isNotEmpty)
IconButton(
visualDensity: const VisualDensity(horizontal: -4),
tooltip: 'Clear',
onPressed: () {
searchController.clear();
setState(() {});
},
icon: Icon(
Icons.close,
size: 20,
color: kSubPeraColor,
),
),
GestureDetector(
onTap: () => _showFilterBottomSheet(context, ref, _theme),
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'),
),
),
),
],
),
),
onChanged: (value) => setState(() {}),
),
),
],
),
),
),
body: RefreshIndicator(
onRefresh: () => refreshData(ref),
child: SingleChildScrollView(
child: Column(
children: [
if (permissionService.hasPermission(Permit.expiredProductReportsRead.value)) ...{
filteredProducts.isNotEmpty
? ListView.separated(
// padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
shrinkWrap: true,
itemCount: filteredProducts.length,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (_, i) {
final product = filteredProducts[i];
final now = DateTime.now();
final firstStock = product.stocks?.isNotEmpty == true ? product.stocks![0] : null;
// Get all matching stocks for this product
final matchingStocks = product.stocks?.where((stock) {
final stockExpireDate =
stock.expireDate != null ? DateTime.tryParse(stock.expireDate!) : null;
if (stockExpireDate == null) return false;
// Check if this stock matches the current filters
if (_isFiltered) {
if (_fromDate != null && stockExpireDate.isBefore(_fromDate!))
return false;
if (_toDate != null && stockExpireDate.isAfter(_toDate!)) return false;
}
if (selectedFilter != 'All') {
int daysUntilExpiration = stockExpireDate.difference(now).inDays;
switch (selectedFilter) {
case 'Expired':
if (!stockExpireDate.isBefore(now)) return false;
break;
case '7 days':
if (!(daysUntilExpiration >= 0 && daysUntilExpiration <= 7)) {
return false;
}
break;
case '15 days':
if (!(daysUntilExpiration > 7 && daysUntilExpiration <= 15)) {
return false;
}
break;
case '30 days':
if (!(daysUntilExpiration > 15 && daysUntilExpiration <= 30)) {
return false;
}
break;
}
}
return true;
}).toList() ??
[];
return Theme(
data: Theme.of(context).copyWith(
dividerColor: Colors.transparent,
),
child: ExpansionTile(
showTrailingIcon: false,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
product.productName.toString(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
),
Text(
'${_lang.sale}: $currency${formatPointNumber(firstStock?.productSalePrice ?? 0)}',
style: _theme.textTheme.bodyMedium,
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${_lang.code}: ${product.productCode ?? 'N/A'}',
style: _theme.textTheme.bodyMedium,
),
Text(
'${_lang.purchase}: $currency${formatPointNumber(firstStock?.productPurchasePrice ?? 0)}',
style: _theme.textTheme.bodyMedium,
),
],
),
],
),
children: matchingStocks.map((stock) {
final stockExpireDate = DateTime.parse(stock.expireDate!);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Batch: ${stock.batchNo ?? 'N/A'}',
style: _theme.textTheme.titleSmall,
),
Text(
'Qty: ${stock.productStock?.toString() ?? '0'}',
style: _theme.textTheme.bodyMedium,
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text.rich(TextSpan(
text: 'Expiry: ',
style: _theme.textTheme.bodyMedium?.copyWith(
color: kPeraColor,
),
children: [
TextSpan(
text: DateFormat('yyyy-MM-dd').format(stockExpireDate),
style: TextStyle(
color: _getExpirationColor(stockExpireDate),
),
),
])),
Text(
getExpirationStatus(stockExpireDate),
style: TextStyle(
color: _getExpirationColor(stockExpireDate),
),
),
],
),
const SizedBox(height: 8),
],
),
);
}).toList(),
),
);
},
separatorBuilder: (BuildContext context, int index) {
return Divider(
thickness: 1,
color: updateBorderColor,
);
},
)
: Center(
child: Text(
_lang.listIsEmpty,
style: _theme.textTheme.bodyMedium?.copyWith(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
} else
Center(child: PermitDenyWidget()),
],
),
),
),
bottomNavigationBar: Container(
color: const Color(0xffFEF0F1),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_lang.stockValue,
style: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
Text(
'$currency${formatPointNumber(totalParPrice)}',
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
),
),
);
},
error: (e, stack) {
return Text(e.toString());
},
loading: () {
return const Center(child: CircularProgressIndicator());
},
);
},
error: (e, stack) => Text(e.toString()),
loading: () => Center(
child: CircularProgressIndicator(),
));
});
}
void _showFilterBottomSheet(BuildContext context, WidgetRef ref, ThemeData theme) {
// Initialize default 7-day range if Custom is selected
if (selectedFilter == 'Custom') {
final now = DateTime.now();
_toDate ??= now;
_fromDate ??= now.subtract(const Duration(days: 7));
fromDateController.text = DateFormat('yyyy-MM-dd').format(_fromDate!);
toDateController.text = DateFormat('yyyy-MM-dd').format(_toDate!);
}
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsetsDirectional.only(start: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
lang.S.of(context).filter,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close, size: 18),
)
],
),
),
const Divider(color: kBorderColor, height: 1),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
DropdownButtonFormField<String>(
value: selectedFilter,
hint: Text(lang.S.of(context).selectOne),
items: [
DropdownMenuItem(
value: 'All',
child: Text(lang.S.of(context).all),
),
DropdownMenuItem(
value: 'Expired',
child: Text(lang.S.of(context).expired),
),
DropdownMenuItem(
value: '7 days',
child: Text(lang.S.of(context).sevenDays),
),
DropdownMenuItem(
value: '15 days',
child: Text(lang.S.of(context).fifteenthDays),
),
DropdownMenuItem(
value: '30 days',
child: Text(lang.S.of(context).thirtyDays),
),
DropdownMenuItem(
value: 'Custom',
child: Text(lang.S.of(context).custom),
),
],
// items: ['All', 'Expired', '7 days', '15 days', '30 days', 'Custom'].map((status) {
// return DropdownMenuItem<String>(
// value: status,
// child: Text(status),
// );
// }).toList(),
onChanged: (value) {
setState(() {
selectedFilter = value!;
if (selectedFilter != 'Custom') {
_fromDate = null;
_toDate = null;
fromDateController.clear();
toDateController.clear();
} else {
// When Custom selected, default 7-day range
final now = DateTime.now();
_toDate = now;
_fromDate = now.subtract(const Duration(days: 7));
fromDateController.text = DateFormat('yyyy-MM-dd').format(_fromDate!);
toDateController.text = DateFormat('yyyy-MM-dd').format(_toDate!);
}
});
},
decoration: InputDecoration(
labelText: lang.S.of(context).expirationStatus,
),
),
const SizedBox(height: 20),
if (_errorMessage != null)
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Text(
_errorMessage!,
style: const TextStyle(color: Colors.red),
),
),
if (selectedFilter == 'Custom') ...[
// const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: GestureDetector(
onTap: () => _selectFromDate(context),
child: TextFormField(
controller: fromDateController,
enabled: false,
style: theme.textTheme.bodyLarge,
decoration: InputDecoration(
labelText: lang.S.of(context).fromDate,
hintText: lang.S.of(context).selectFDate,
suffixIcon: const Icon(Icons.calendar_month_rounded),
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: GestureDetector(
onTap: () => _selectToDate(context),
child: TextFormField(
controller: toDateController,
style: theme.textTheme.bodyLarge,
enabled: false,
decoration: InputDecoration(
labelText: lang.S.of(context).toDate,
hintText: lang.S.of(context).selectToDate,
suffixIcon: const Icon(Icons.calendar_month_rounded),
),
),
),
),
],
),
const SizedBox(height: 20),
],
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _clearFilters,
child: Text(lang.S.of(context).clear),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: _applyFilters,
child: Text(lang.S.of(context).apply),
),
),
],
),
],
),
),
],
),
);
},
);
},
);
}
}
Color _getExpirationColor(DateTime expireDate) {
final DateTime now = DateTime.now();
final Duration difference = expireDate.difference(now);
if (difference.isNegative) {
return Colors.red; // Expired
} else if (difference.inDays <= 7) {
return Colors.orange; // Expiring soon (7 days or less)
} else if (difference.inDays <= 30) {
return Colors.amber; // Expiring within a month
} else {
return Colors.green; // Not expiring soon
}
}
String getExpirationStatus(DateTime date) {
final DateTime now = DateTime.now();
final Duration difference = date.difference(now);
if (difference.isNegative) {
return 'Expired ${difference.inDays.abs()} days ago';
} else if (difference.inDays == 0) {
return 'Expires today';
} else if (difference.inDays == 1) {
return 'Expires tomorrow';
} else if (difference.inDays <= 7) {
return 'Expires in ${difference.inDays} days';
} else if (difference.inDays <= 30) {
return 'Expires in ${difference.inDays} days';
} else {
return 'Expires in ${difference.inDays} days';
}
}

View File

@@ -0,0 +1,528 @@
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:iconly/iconly.dart';
import 'package:intl/intl.dart';
import 'package:intl/intl.dart' as intl;
import 'package:mobile_pos/generated/l10n.dart' as l;
import '../../../../GlobalComponents/glonal_popup.dart';
import '../../../../Provider/profile_provider.dart';
import '../../../../Provider/transactions_provider.dart';
import '../../../../constant.dart';
import '../../../../currency.dart';
import '../../../../model/product_history_model.dart' as phlm;
import '../../../../pdf_report/transactions/product_wise_purchase_history_details_report_pdf.dart';
import '../../../../service/check_user_role_permission_provider.dart';
class ProductPurchaseHistoryReportDetails extends ConsumerStatefulWidget {
const ProductPurchaseHistoryReportDetails({super.key, required this.data});
final phlm.ProductHistoryItemModel data;
@override
ConsumerState<ProductPurchaseHistoryReportDetails> createState() => _ProductPurchaseHistoryReportDetailsState();
}
class _ProductPurchaseHistoryReportDetailsState extends ConsumerState<ProductPurchaseHistoryReportDetails> {
final TextEditingController fromDateController = TextEditingController();
final TextEditingController toDateController = TextEditingController();
final Map<String, String> dateOptions = {
'today': l.S.current.today,
'yesterday': l.S.current.yesterday,
'last_seven_days': l.S.current.last7Days,
'last_thirty_days': l.S.current.last30Days,
'current_month': l.S.current.currentMonth,
'last_month': l.S.current.lastMonth,
'current_year': l.S.current.currentYear,
'custom_date': l.S.current.customerDate,
};
String selectedTime = 'today';
bool _isRefreshing = false;
bool _showCustomDatePickers = false;
DateTime? fromDate;
DateTime? toDate;
String searchCustomer = '';
/// Generates the date range string for the provider
FilterModel _getDateRangeFilter() {
if (_showCustomDatePickers && fromDate != null && toDate != null) {
return FilterModel(
duration: 'custom_date',
fromDate: DateFormat('yyyy-MM-dd', 'en_US').format(fromDate!),
toDate: DateFormat('yyyy-MM-dd', 'en_US').format(toDate!),
);
} else {
return FilterModel(duration: selectedTime.toLowerCase());
}
}
Future<void> _selectDate({
required BuildContext context,
required bool isFrom,
}) async {
final DateTime? picked = await showDatePicker(
context: context,
firstDate: DateTime(2021),
lastDate: DateTime.now(),
initialDate: isFrom ? fromDate ?? DateTime.now() : toDate ?? DateTime.now(),
);
if (picked != null) {
setState(() {
if (isFrom) {
fromDate = picked;
fromDateController.text = DateFormat('yyyy-MM-dd').format(picked);
} else {
toDate = picked;
toDateController.text = DateFormat('yyyy-MM-dd').format(picked);
}
});
if (fromDate != null && toDate != null) _refreshFilteredProvider();
}
}
Future<void> _refreshFilteredProvider() async {
if (_isRefreshing) return;
_isRefreshing = true;
try {
final filter = _getDateRangeFilter();
ref.refresh(filteredProductPurchaseHistoryReportDetailsProvider((productId: widget.data.id!, filter: filter)));
await Future.delayed(const Duration(milliseconds: 300)); // small delay
} finally {
_isRefreshing = false;
}
}
@override
void dispose() {
fromDateController.dispose();
toDateController.dispose();
super.dispose();
}
void _updateDateUI(DateTime? from, DateTime? to) {
setState(() {
fromDate = from;
toDate = to;
fromDateController.text = from != null ? DateFormat('yyyy-MM-dd').format(from) : '';
toDateController.text = to != null ? DateFormat('yyyy-MM-dd').format(to) : '';
});
}
void _setDateRangeFromDropdown(String value) {
final now = DateTime.now();
switch (value) {
case 'today':
_updateDateUI(now, now);
break;
case 'yesterday':
final y = now.subtract(const Duration(days: 1));
_updateDateUI(y, y);
break;
case 'last_seven_days':
_updateDateUI(
now.subtract(const Duration(days: 6)),
now,
);
break;
case 'last_thirty_days':
_updateDateUI(
now.subtract(const Duration(days: 29)),
now,
);
break;
case 'current_month':
_updateDateUI(
DateTime(now.year, now.month, 1),
now,
);
break;
case 'last_month':
final first = DateTime(now.year, now.month - 1, 1);
final last = DateTime(now.year, now.month, 0);
_updateDateUI(first, last);
break;
case 'current_year':
_updateDateUI(
DateTime(now.year, 1, 1),
now,
);
break;
case 'custom_date':
// Custom: User will select manually
_updateDateUI(null, null);
break;
}
}
@override
void initState() {
super.initState();
final now = DateTime.now();
// Set initial From and To date = TODAY
fromDate = now;
toDate = now;
fromDateController.text = DateFormat('yyyy-MM-dd').format(now);
toDateController.text = DateFormat('yyyy-MM-dd').format(now);
}
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
final _lang = l.S.of(context);
return Consumer(
builder: (_, ref, watch) {
final providerData = ref.watch(
filteredProductPurchaseHistoryReportDetailsProvider(
(productId: widget.data.id!, filter: _getDateRangeFilter())),
);
final personalData = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
return personalData.when(
data: (business) {
return providerData.when(
data: (tx) {
final _items = [...?tx.items];
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
backgroundColor: Colors.white,
title: Text(widget.data.name ?? _lang.product),
actions: [
IconButton(
onPressed: () {
if (!permissionService.hasPermission(Permit.lossProfitsRead.value)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text(_lang.youDoNotHavePermissionProfitAndLoss),
),
);
return;
}
if ((tx.items?.isNotEmpty == true) || (tx.items?.isNotEmpty == true)) {
generateProductWisePurchaseHistoryDetailsReportPdf(
context,
tx,
business,
fromDate,
toDate,
);
} else {
EasyLoading.showError(_lang.listIsEmpty);
}
},
icon: HugeIcon(icon: HugeIcons.strokeRoundedPdf02, color: kSecondayColor),
),
/*
IconButton(
onPressed: () {
if (!permissionService.hasPermission(Permit.lossProfitsRead.value)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text('You do not have permission of loss profit.'),
),
);
return;
}
if ((tx.data?.isNotEmpty == true) || (tx.data?.isNotEmpty == true)) {
// generateLossProfitReportExcel(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showError('List is empty');
}
},
icon: SvgPicture.asset('assets/excel.svg'),
),
*/
SizedBox(width: 8),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(50),
child: Column(
children: [
Divider(thickness: 1, color: kBottomBorder, height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
flex: 2,
child: Row(
children: [
Icon(IconlyLight.calendar, color: kPeraColor, size: 20),
SizedBox(width: 3),
GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: true);
}
},
child: Text(
fromDate != null ? DateFormat('dd MMM yyyy').format(fromDate!) : _lang.from,
style: Theme.of(context).textTheme.bodyMedium,
),
),
SizedBox(width: 4),
Text(
_lang.to,
style: _theme.textTheme.titleSmall,
),
SizedBox(width: 4),
Flexible(
child: GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: false);
}
},
child: Text(
toDate != null ? DateFormat('dd MMM yyyy').format(toDate!) : _lang.to,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
],
),
),
SizedBox(width: 2),
RotatedBox(
quarterTurns: 1,
child: Container(
height: 1,
width: 20,
color: kSubPeraColor,
),
),
SizedBox(width: 2),
Expanded(
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
iconSize: 20,
value: selectedTime,
isExpanded: true,
items: dateOptions.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(
entry.value,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium,
),
);
}).toList(),
onChanged: (value) {
if (value == null) return;
setState(() {
selectedTime = value;
_showCustomDatePickers = value == 'custom_date';
});
if (value != 'custom_date') {
_setDateRangeFromDropdown(value);
_refreshFilteredProvider();
}
},
),
),
),
],
),
),
Divider(thickness: 1, color: kBottomBorder, height: 1),
],
),
),
iconTheme: const IconThemeData(color: Colors.black),
centerTitle: true,
elevation: 0.0,
),
body: RefreshIndicator(
onRefresh: _refreshFilteredProvider,
child: Column(
children: [
DefaultTextStyle.merge(
style: _theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 15,
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xffF7F7F7),
border: Border(bottom: Divider.createBorderSide(context)),
),
child: Row(
children: [
Expanded(
flex: 3,
child: Text(_lang.invoice, textAlign: TextAlign.start),
),
Expanded(
flex: 2,
child: Text(_lang.type, textAlign: TextAlign.center),
),
Expanded(
flex: 2,
child: Text(
_lang.qty,
textAlign: TextAlign.center,
),
),
Expanded(
flex: 2,
child: Text(
_lang.cost,
textAlign: TextAlign.center,
),
),
],
),
),
),
Expanded(
child: ListView.builder(
itemCount: _items.length,
itemBuilder: (context, index) {
final _item = _items[index];
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 6,
),
decoration: BoxDecoration(
border: Border(
bottom: Divider.createBorderSide(context),
),
),
child: Row(
children: [
Expanded(
flex: 3,
child: Text.rich(
TextSpan(
text: "${_item.invoiceNo ?? "N/A"}\n",
children: [
TextSpan(
text: _item.transactionDate == null
? null
: intl.DateFormat("dd MMM yyyy").format(_item.transactionDate!),
style: TextStyle(
fontWeight: FontWeight.normal,
color: const Color(0xff4B5563),
),
),
],
),
style: _theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
),
Expanded(
flex: 2,
child: Text(
_item.type ?? "N/A",
textAlign: TextAlign.center,
),
),
Expanded(
flex: 2,
child: Text(
(_item.quantities ?? 0).toString(),
textAlign: TextAlign.center,
),
),
Expanded(
flex: 2,
child: Text(
"$currency${formatPointNumber(_item.purchasePrice ?? 0)}",
textAlign: TextAlign.center,
),
),
],
),
);
},
),
)
],
),
),
bottomNavigationBar: DefaultTextStyle.merge(
style: _theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: kTextColor,
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
border: Border(top: Divider.createBorderSide(context)),
),
child: Row(
children: [
Expanded(
flex: 3,
child: Text(_lang.total, textAlign: TextAlign.start),
),
Expanded(
flex: 2,
child: const SizedBox.shrink(),
),
Expanded(
flex: 2,
child: Text(
(tx.totalQuantities ?? 0).toString(),
textAlign: TextAlign.center,
),
),
Expanded(
flex: 2,
child: Text(
"$currency${formatPointNumber(tx.totalPurchasePrice ?? 0)}",
textAlign: TextAlign.center,
),
),
],
),
),
),
),
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: () => Center(child: CircularProgressIndicator()),
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: () => Center(child: CircularProgressIndicator()),
);
},
);
}
}

View File

@@ -0,0 +1,524 @@
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:iconly/iconly.dart';
import 'package:intl/intl.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 '../../../../Provider/transactions_provider.dart';
import '../../../../constant.dart';
import '../../../../currency.dart';
import '../../../../pdf_report/transactions/product_wise_purchase_history_list_report_pdf.dart';
import '../../../../service/check_user_role_permission_provider.dart';
import 'product_purchase_history_report_details.dart';
class ProductPurchaseHistoryReportList extends ConsumerStatefulWidget {
const ProductPurchaseHistoryReportList({super.key, this.fromReport});
final bool? fromReport;
@override
ConsumerState<ProductPurchaseHistoryReportList> createState() => _ProductPurchaseHistoryReportListState();
}
class _ProductPurchaseHistoryReportListState extends ConsumerState<ProductPurchaseHistoryReportList> {
final TextEditingController fromDateController = TextEditingController();
final TextEditingController toDateController = TextEditingController();
final Map<String, String> dateOptions = {
'today': l.S.current.today,
'yesterday': l.S.current.yesterday,
'last_seven_days': l.S.current.last7Days,
'last_thirty_days': l.S.current.last30Days,
'current_month': l.S.current.currentMonth,
'last_month': l.S.current.lastMonth,
'current_year': l.S.current.currentYear,
'custom_date': l.S.current.customerDate,
};
String selectedTime = 'today';
bool _isRefreshing = false;
bool _showCustomDatePickers = false;
DateTime? fromDate;
DateTime? toDate;
String searchCustomer = '';
/// Generates the date range string for the provider
FilterModel _getDateRangeFilter() {
if (_showCustomDatePickers && fromDate != null && toDate != null) {
return FilterModel(
duration: 'custom_date',
fromDate: DateFormat('yyyy-MM-dd', 'en_US').format(fromDate!),
toDate: DateFormat('yyyy-MM-dd', 'en_US').format(toDate!),
);
} else {
return FilterModel(duration: selectedTime.toLowerCase());
}
}
Future<void> _selectDate({
required BuildContext context,
required bool isFrom,
}) async {
final DateTime? picked = await showDatePicker(
context: context,
firstDate: DateTime(2021),
lastDate: DateTime.now(),
initialDate: isFrom ? fromDate ?? DateTime.now() : toDate ?? DateTime.now(),
);
if (picked != null) {
setState(() {
if (isFrom) {
fromDate = picked;
fromDateController.text = DateFormat('yyyy-MM-dd').format(picked);
} else {
toDate = picked;
toDateController.text = DateFormat('yyyy-MM-dd').format(picked);
}
});
if (fromDate != null && toDate != null) _refreshFilteredProvider();
}
}
Future<void> _refreshFilteredProvider() async {
if (_isRefreshing) return;
_isRefreshing = true;
try {
final filter = _getDateRangeFilter();
ref.refresh(filteredProductSaleHistoryReportProvider(filter));
await Future.delayed(const Duration(milliseconds: 300)); // small delay
} finally {
_isRefreshing = false;
}
}
@override
void dispose() {
fromDateController.dispose();
toDateController.dispose();
super.dispose();
}
void _updateDateUI(DateTime? from, DateTime? to) {
setState(() {
fromDate = from;
toDate = to;
fromDateController.text = from != null ? DateFormat('yyyy-MM-dd').format(from) : '';
toDateController.text = to != null ? DateFormat('yyyy-MM-dd').format(to) : '';
});
}
void _setDateRangeFromDropdown(String value) {
final now = DateTime.now();
switch (value) {
case 'today':
_updateDateUI(now, now);
break;
case 'yesterday':
final y = now.subtract(const Duration(days: 1));
_updateDateUI(y, y);
break;
case 'last_seven_days':
_updateDateUI(
now.subtract(const Duration(days: 6)),
now,
);
break;
case 'last_thirty_days':
_updateDateUI(
now.subtract(const Duration(days: 29)),
now,
);
break;
case 'current_month':
_updateDateUI(
DateTime(now.year, now.month, 1),
now,
);
break;
case 'last_month':
final first = DateTime(now.year, now.month - 1, 1);
final last = DateTime(now.year, now.month, 0);
_updateDateUI(first, last);
break;
case 'current_year':
_updateDateUI(
DateTime(now.year, 1, 1),
now,
);
break;
case 'custom_date':
// Custom: User will select manually
_updateDateUI(null, null);
break;
}
}
@override
void initState() {
super.initState();
final now = DateTime.now();
// Set initial From and To date = TODAY
fromDate = now;
toDate = now;
fromDateController.text = DateFormat('yyyy-MM-dd').format(now);
toDateController.text = DateFormat('yyyy-MM-dd').format(now);
}
@override
Widget build(BuildContext context) {
final _lang = l.S.of(context);
final _theme = Theme.of(context);
return Consumer(
builder: (_, ref, watch) {
final providerData = ref.watch(filteredProductSaleHistoryReportProvider(_getDateRangeFilter()));
final personalData = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
return personalData.when(
data: (business) {
return providerData.when(
data: (tx) {
final _items = [...?tx.items];
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
backgroundColor: Colors.white,
title: Text(_lang.productPurchaseHistory),
actions: [
IconButton(
onPressed: () {
if ((tx.items?.isNotEmpty == true) || (tx.items?.isNotEmpty == true)) {
generateProductWisePurchaseHistoryReportPdf(context, tx, business, fromDate, toDate);
} else {
EasyLoading.showError(_lang.listIsEmpty);
}
},
icon: HugeIcon(icon: HugeIcons.strokeRoundedPdf02, color: kSecondayColor),
),
/*
IconButton(
onPressed: () {
if (!permissionService.hasPermission(Permit.lossProfitsRead.value)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text('You do not have permission of loss profit.'),
),
);
return;
}
if ((tx.data?.isNotEmpty == true) || (tx.data?.isNotEmpty == true)) {
// generateLossProfitReportExcel(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showError('List is empty');
}
},
icon: SvgPicture.asset('assets/excel.svg'),
),
*/
SizedBox(width: 8),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(50),
child: Column(
children: [
Divider(thickness: 1, color: kBottomBorder, height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
flex: 2,
child: Row(
children: [
Icon(IconlyLight.calendar, color: kPeraColor, size: 20),
SizedBox(width: 3),
GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: true);
}
},
child: Text(
fromDate != null ? DateFormat('dd MMM yyyy').format(fromDate!) : _lang.from,
style: Theme.of(context).textTheme.bodyMedium,
),
),
SizedBox(width: 4),
Text(
_lang.to,
style: _theme.textTheme.titleSmall,
),
SizedBox(width: 4),
Flexible(
child: GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: false);
}
},
child: Text(
toDate != null ? DateFormat('dd MMM yyyy').format(toDate!) : _lang.to,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
],
),
),
SizedBox(width: 2),
RotatedBox(
quarterTurns: 1,
child: Container(
height: 1,
width: 20,
color: kSubPeraColor,
),
),
SizedBox(width: 2),
Expanded(
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
iconSize: 20,
value: selectedTime,
isExpanded: true,
items: dateOptions.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(
entry.value,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium,
),
);
}).toList(),
onChanged: (value) {
if (value == null) return;
setState(() {
selectedTime = value;
_showCustomDatePickers = value == 'custom_date';
});
if (value != 'custom_date') {
_setDateRangeFromDropdown(value);
_refreshFilteredProvider();
}
},
),
),
),
],
),
),
Divider(thickness: 1, color: kBottomBorder, height: 1),
],
),
),
iconTheme: const IconThemeData(color: Colors.black),
centerTitle: true,
elevation: 0.0,
),
body: RefreshIndicator(
onRefresh: _refreshFilteredProvider,
child: Column(
children: [
DefaultTextStyle.merge(
style: _theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 15,
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xffF7F7F7),
border: Border(bottom: Divider.createBorderSide(context)),
),
child: Row(
children: [
Expanded(
flex: 3,
child: Text(_lang.name, textAlign: TextAlign.start),
),
Expanded(
flex: 3,
child: Text(_lang.purchase, textAlign: TextAlign.center),
),
Expanded(
flex: 2,
child: Text(
_lang.sold,
textAlign: TextAlign.center,
),
),
Expanded(
flex: 3,
child: Text(
_lang.remaining,
textAlign: TextAlign.end,
),
),
],
),
),
),
Expanded(
child: ListView.builder(
itemCount: _items.length,
itemBuilder: (context, index) {
final _item = _items[index];
return GestureDetector(
onTap: () async {
return Navigator.of(context).push<void>(MaterialPageRoute(
builder: (_) => ProductPurchaseHistoryReportDetails(data: _item),
));
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 6,
),
decoration: BoxDecoration(
border: Border(
bottom: Divider.createBorderSide(context),
),
),
child: Row(
children: [
Expanded(
flex: 3,
child: Text.rich(
TextSpan(
text: "${_item.name ?? "N/A"}\n",
children: [
TextSpan(
text: '${_lang.cost}: ',
children: [
TextSpan(
text:
"$currency${formatPointNumber(_item.purchaseQuantity ?? 0)}",
style:
TextStyle(fontWeight: FontWeight.w500, color: Colors.black),
)
],
style: TextStyle(
fontWeight: FontWeight.normal,
color: const Color(0xff4B5563),
),
),
],
),
style: _theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
),
Expanded(
flex: 3,
child: Text(
(_item.purchaseQuantity ?? 0).toString(),
textAlign: TextAlign.center,
),
),
Expanded(
flex: 2,
child: Text(
formatPointNumber((_item.saleQuantity ?? 0), addComma: true),
textAlign: TextAlign.center,
),
),
Expanded(
flex: 3,
child: Text(
'$currency${formatPointNumber((_item.remainingQuantity ?? 0), addComma: true)}',
textAlign: TextAlign.end,
),
),
],
),
),
);
},
),
)
],
),
),
bottomNavigationBar: DefaultTextStyle.merge(
style: _theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: kTextColor,
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
border: Border(top: Divider.createBorderSide(context)),
),
child: Row(
children: [
Expanded(
flex: 3,
child: Text(_lang.total, textAlign: TextAlign.start),
),
Expanded(
flex: 2,
child: Text("${tx.totalPurchaseQuantity ?? 0}", textAlign: TextAlign.center),
),
Expanded(
flex: 2,
child: Text(
"${tx.totalSaleQuantity ?? 0}",
textAlign: TextAlign.center,
),
),
Expanded(
flex: 2,
child: Text(
"$currency${tx.totalRemainingQuantity}",
textAlign: TextAlign.end,
style: TextStyle(color: DAppColors.kError),
),
),
],
),
),
),
),
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: () => Center(child: CircularProgressIndicator()),
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: () => Center(child: CircularProgressIndicator()),
);
},
);
}
}

View File

@@ -0,0 +1,513 @@
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:iconly/iconly.dart';
import 'package:intl/intl.dart';
import 'package:intl/intl.dart' as intl;
import 'package:mobile_pos/generated/l10n.dart' as l;
import '../../../../GlobalComponents/glonal_popup.dart';
import '../../../../Provider/profile_provider.dart';
import '../../../../Provider/transactions_provider.dart';
import '../../../../constant.dart';
import '../../../../core/theme/_app_colors.dart';
import '../../../../currency.dart';
import '../../../../model/product_history_model.dart' as phlm;
import '../../../../pdf_report/transactions/product_wise_sale_history_details_report_pdf.dart';
import '../../../../service/check_user_role_permission_provider.dart';
class ProductSaleHistoryReportDetails extends ConsumerStatefulWidget {
const ProductSaleHistoryReportDetails({super.key, required this.data});
final phlm.ProductHistoryItemModel data;
@override
ConsumerState<ProductSaleHistoryReportDetails> createState() => _ProductSaleHistoryReportDetailsState();
}
class _ProductSaleHistoryReportDetailsState extends ConsumerState<ProductSaleHistoryReportDetails> {
final TextEditingController fromDateController = TextEditingController();
final TextEditingController toDateController = TextEditingController();
final Map<String, String> dateOptions = {
'today': l.S.current.today,
'yesterday': l.S.current.yesterday,
'last_seven_days': l.S.current.last7Days,
'last_thirty_days': l.S.current.last30Days,
'current_month': l.S.current.currentMonth,
'last_month': l.S.current.lastMonth,
'current_year': l.S.current.currentYear,
'custom_date': l.S.current.customerDate,
};
String selectedTime = 'today';
bool _isRefreshing = false;
bool _showCustomDatePickers = false;
DateTime? fromDate;
DateTime? toDate;
String searchCustomer = '';
/// Generates the date range string for the provider
FilterModel _getDateRangeFilter() {
if (_showCustomDatePickers && fromDate != null && toDate != null) {
return FilterModel(
duration: 'custom_date',
fromDate: DateFormat('yyyy-MM-dd', 'en_US').format(fromDate!),
toDate: DateFormat('yyyy-MM-dd', 'en_US').format(toDate!),
);
} else {
return FilterModel(duration: selectedTime.toLowerCase());
}
}
Future<void> _selectDate({
required BuildContext context,
required bool isFrom,
}) async {
final DateTime? picked = await showDatePicker(
context: context,
firstDate: DateTime(2021),
lastDate: DateTime.now(),
initialDate: isFrom ? fromDate ?? DateTime.now() : toDate ?? DateTime.now(),
);
if (picked != null) {
setState(() {
if (isFrom) {
fromDate = picked;
fromDateController.text = DateFormat('yyyy-MM-dd').format(picked);
} else {
toDate = picked;
toDateController.text = DateFormat('yyyy-MM-dd').format(picked);
}
});
if (fromDate != null && toDate != null) _refreshFilteredProvider();
}
}
Future<void> _refreshFilteredProvider() async {
if (_isRefreshing) return;
_isRefreshing = true;
try {
final filter = _getDateRangeFilter();
ref.refresh(filteredProductSaleHistoryReportDetailsProvider((productId: widget.data.id!, filter: filter)));
await Future.delayed(const Duration(milliseconds: 300)); // small delay
} finally {
_isRefreshing = false;
}
}
@override
void dispose() {
fromDateController.dispose();
toDateController.dispose();
super.dispose();
}
void _updateDateUI(DateTime? from, DateTime? to) {
setState(() {
fromDate = from;
toDate = to;
fromDateController.text = from != null ? DateFormat('yyyy-MM-dd').format(from) : '';
toDateController.text = to != null ? DateFormat('yyyy-MM-dd').format(to) : '';
});
}
void _setDateRangeFromDropdown(String value) {
final now = DateTime.now();
switch (value) {
case 'today':
_updateDateUI(now, now);
break;
case 'yesterday':
final y = now.subtract(const Duration(days: 1));
_updateDateUI(y, y);
break;
case 'last_seven_days':
_updateDateUI(
now.subtract(const Duration(days: 6)),
now,
);
break;
case 'last_thirty_days':
_updateDateUI(
now.subtract(const Duration(days: 29)),
now,
);
break;
case 'current_month':
_updateDateUI(
DateTime(now.year, now.month, 1),
now,
);
break;
case 'last_month':
final first = DateTime(now.year, now.month - 1, 1);
final last = DateTime(now.year, now.month, 0);
_updateDateUI(first, last);
break;
case 'current_year':
_updateDateUI(
DateTime(now.year, 1, 1),
now,
);
break;
case 'custom_date':
// Custom: User will select manually
_updateDateUI(null, null);
break;
}
}
@override
void initState() {
super.initState();
final now = DateTime.now();
// Set initial From and To date = TODAY
fromDate = now;
toDate = now;
fromDateController.text = DateFormat('yyyy-MM-dd').format(now);
toDateController.text = DateFormat('yyyy-MM-dd').format(now);
}
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
final _lang = l.S.of(context);
return Consumer(
builder: (_, ref, watch) {
final providerData = ref.watch(
filteredProductSaleHistoryReportDetailsProvider((productId: widget.data.id!, filter: _getDateRangeFilter())),
);
final personalData = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
return personalData.when(
data: (business) {
return providerData.when(
data: (tx) {
final _items = [...?tx.items];
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
backgroundColor: Colors.white,
title: Text(widget.data.name ?? _lang.product),
actions: [
IconButton(
onPressed: () {
if ((tx.items?.isNotEmpty == true) || (tx.items?.isNotEmpty == true)) {
generateProductWiseSaleHistoryDetailsReportPdf(context, tx, business, fromDate, toDate);
} else {
EasyLoading.showError(_lang.listIsEmpty);
}
},
icon: HugeIcon(icon: HugeIcons.strokeRoundedPdf02, color: kSecondayColor),
),
/*
IconButton(
onPressed: () {
if (!permissionService.hasPermission(Permit.lossProfitsRead.value)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text('You do not have permission of loss profit.'),
),
);
return;
}
if ((tx.data?.isNotEmpty == true) || (tx.data?.isNotEmpty == true)) {
// generateLossProfitReportExcel(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showError('List is empty');
}
},
icon: SvgPicture.asset('assets/excel.svg'),
),
*/
SizedBox(width: 8),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(50),
child: Column(
children: [
Divider(thickness: 1, color: kBottomBorder, height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
flex: 2,
child: Row(
children: [
Icon(IconlyLight.calendar, color: kPeraColor, size: 20),
SizedBox(width: 3),
GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: true);
}
},
child: Text(
fromDate != null ? DateFormat('dd MMM yyyy').format(fromDate!) : _lang.from,
style: Theme.of(context).textTheme.bodyMedium,
),
),
SizedBox(width: 4),
Text(
_lang.to,
style: _theme.textTheme.titleSmall,
),
SizedBox(width: 4),
Flexible(
child: GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: false);
}
},
child: Text(
toDate != null ? DateFormat('dd MMM yyyy').format(toDate!) : _lang.to,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
],
),
),
SizedBox(width: 2),
RotatedBox(
quarterTurns: 1,
child: Container(
height: 1,
width: 20,
color: kSubPeraColor,
),
),
SizedBox(width: 2),
Expanded(
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
iconSize: 20,
value: selectedTime,
isExpanded: true,
items: dateOptions.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(
entry.value,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium,
),
);
}).toList(),
onChanged: (value) {
if (value == null) return;
setState(() {
selectedTime = value;
_showCustomDatePickers = value == 'custom_date';
});
if (value != 'custom_date') {
_setDateRangeFromDropdown(value);
_refreshFilteredProvider();
}
},
),
),
),
],
),
),
Divider(thickness: 1, color: kBottomBorder, height: 1),
],
),
),
iconTheme: const IconThemeData(color: Colors.black),
centerTitle: true,
elevation: 0.0,
),
body: RefreshIndicator(
onRefresh: _refreshFilteredProvider,
child: Column(
children: [
DefaultTextStyle.merge(
style: _theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 15,
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xffF7F7F7),
border: Border(bottom: Divider.createBorderSide(context)),
),
child: Row(
children: [
Expanded(
flex: 3,
child: Text(_lang.invoice, textAlign: TextAlign.start),
),
Expanded(
flex: 2,
child: Text(_lang.qty, textAlign: TextAlign.center),
),
Expanded(
flex: 2,
child: Text(
_lang.cost,
textAlign: TextAlign.center,
),
),
Expanded(
flex: 2,
child: Text(
_lang.sale,
textAlign: TextAlign.center,
),
),
],
),
),
),
Expanded(
child: ListView.builder(
itemCount: _items.length,
itemBuilder: (context, index) {
final _item = _items[index];
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 6,
),
decoration: BoxDecoration(
border: Border(
bottom: Divider.createBorderSide(context),
),
),
child: Row(
children: [
Expanded(
flex: 3,
child: Text.rich(
TextSpan(
text: "${_item.invoiceNo ?? "N/A"}\n",
children: [
TextSpan(
text: _item.transactionDate == null
? null
: intl.DateFormat("dd MMM yyyy").format(_item.transactionDate!),
style: TextStyle(
fontWeight: FontWeight.normal,
color: const Color(0xff4B5563),
),
),
],
),
style: _theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
),
Expanded(
flex: 2,
child: Text(
(_item.quantities ?? 0).toString(),
textAlign: TextAlign.center,
),
),
Expanded(
flex: 2,
child: Text(
"$currency${formatPointNumber(_item.purchasePrice ?? 0)}",
textAlign: TextAlign.center,
),
),
Expanded(
flex: 2,
child: Text(
"$currency${formatPointNumber(_item.salePrice ?? 0)}",
textAlign: TextAlign.end,
),
),
],
),
);
},
),
)
],
),
),
bottomNavigationBar: DefaultTextStyle.merge(
style: _theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: kTextColor,
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
border: Border(top: Divider.createBorderSide(context)),
),
child: Row(
children: [
Expanded(
flex: 3,
child: Text(_lang.total, textAlign: TextAlign.start),
),
Expanded(
flex: 2,
child: Text("${tx.totalQuantities ?? 0}", textAlign: TextAlign.center),
),
Expanded(
flex: 2,
child: Text(
"$currency${formatPointNumber(tx.totalPurchasePrice ?? 0)}",
textAlign: TextAlign.center,
),
),
Expanded(
flex: 2,
child: Text(
"$currency${formatPointNumber(tx.totalSalePrice ?? 0)}",
textAlign: TextAlign.center,
style: TextStyle(color: DAppColors.kError),
),
),
],
),
),
),
),
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: () => Center(child: CircularProgressIndicator()),
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: () => Center(child: CircularProgressIndicator()),
);
},
);
}
}

View File

@@ -0,0 +1,523 @@
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:iconly/iconly.dart';
import 'package:intl/intl.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 '../../../../Provider/transactions_provider.dart';
import '../../../../constant.dart';
import '../../../../currency.dart';
import '../../../../pdf_report/transactions/product_wise_sale_history_list_report_pdf.dart';
import '../../../../service/check_user_role_permission_provider.dart';
import 'product_sale_history_report_details.dart';
class ProductSaleHistoryReportList extends ConsumerStatefulWidget {
const ProductSaleHistoryReportList({super.key, this.fromReport});
final bool? fromReport;
@override
ConsumerState<ProductSaleHistoryReportList> createState() => _ProductSaleHistoryReportListState();
}
class _ProductSaleHistoryReportListState extends ConsumerState<ProductSaleHistoryReportList> {
final TextEditingController fromDateController = TextEditingController();
final TextEditingController toDateController = TextEditingController();
final Map<String, String> dateOptions = {
'today': l.S.current.today,
'yesterday': l.S.current.yesterday,
'last_seven_days': l.S.current.last7Days,
'last_thirty_days': l.S.current.last30Days,
'current_month': l.S.current.currentMonth,
'last_month': l.S.current.lastMonth,
'current_year': l.S.current.currentYear,
'custom_date': l.S.current.customerDate,
};
String selectedTime = 'today';
bool _isRefreshing = false;
bool _showCustomDatePickers = false;
DateTime? fromDate;
DateTime? toDate;
String searchCustomer = '';
/// Generates the date range string for the provider
FilterModel _getDateRangeFilter() {
if (_showCustomDatePickers && fromDate != null && toDate != null) {
return FilterModel(
duration: 'custom_date',
fromDate: DateFormat('yyyy-MM-dd', 'en_US').format(fromDate!),
toDate: DateFormat('yyyy-MM-dd', 'en_US').format(toDate!),
);
} else {
return FilterModel(duration: selectedTime.toLowerCase());
}
}
Future<void> _selectDate({
required BuildContext context,
required bool isFrom,
}) async {
final DateTime? picked = await showDatePicker(
context: context,
firstDate: DateTime(2021),
lastDate: DateTime.now(),
initialDate: isFrom ? fromDate ?? DateTime.now() : toDate ?? DateTime.now(),
);
if (picked != null) {
setState(() {
if (isFrom) {
fromDate = picked;
fromDateController.text = DateFormat('yyyy-MM-dd').format(picked);
} else {
toDate = picked;
toDateController.text = DateFormat('yyyy-MM-dd').format(picked);
}
});
if (fromDate != null && toDate != null) _refreshFilteredProvider();
}
}
Future<void> _refreshFilteredProvider() async {
if (_isRefreshing) return;
_isRefreshing = true;
try {
final filter = _getDateRangeFilter();
ref.refresh(filteredProductSaleHistoryReportProvider(filter));
await Future.delayed(const Duration(milliseconds: 300)); // small delay
} finally {
_isRefreshing = false;
}
}
@override
void dispose() {
fromDateController.dispose();
toDateController.dispose();
super.dispose();
}
void _updateDateUI(DateTime? from, DateTime? to) {
setState(() {
fromDate = from;
toDate = to;
fromDateController.text = from != null ? DateFormat('yyyy-MM-dd').format(from) : '';
toDateController.text = to != null ? DateFormat('yyyy-MM-dd').format(to) : '';
});
}
void _setDateRangeFromDropdown(String value) {
final now = DateTime.now();
switch (value) {
case 'today':
_updateDateUI(now, now);
break;
case 'yesterday':
final y = now.subtract(const Duration(days: 1));
_updateDateUI(y, y);
break;
case 'last_seven_days':
_updateDateUI(
now.subtract(const Duration(days: 6)),
now,
);
break;
case 'last_thirty_days':
_updateDateUI(
now.subtract(const Duration(days: 29)),
now,
);
break;
case 'current_month':
_updateDateUI(
DateTime(now.year, now.month, 1),
now,
);
break;
case 'last_month':
final first = DateTime(now.year, now.month - 1, 1);
final last = DateTime(now.year, now.month, 0);
_updateDateUI(first, last);
break;
case 'current_year':
_updateDateUI(
DateTime(now.year, 1, 1),
now,
);
break;
case 'custom_date':
// Custom: User will select manually
_updateDateUI(null, null);
break;
}
}
@override
void initState() {
super.initState();
final now = DateTime.now();
// Set initial From and To date = TODAY
fromDate = now;
toDate = now;
fromDateController.text = DateFormat('yyyy-MM-dd').format(now);
toDateController.text = DateFormat('yyyy-MM-dd').format(now);
}
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
final _lang = l.S.of(context);
return Consumer(
builder: (_, ref, watch) {
final providerData = ref.watch(filteredProductSaleHistoryReportProvider(_getDateRangeFilter()));
final personalData = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
return personalData.when(
data: (business) {
return providerData.when(
data: (tx) {
final _items = [...?tx.items];
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
backgroundColor: Colors.white,
title: Text(_lang.productSaleHistory),
actions: [
IconButton(
onPressed: () {
if ((tx.items?.isNotEmpty == true) || (tx.items?.isNotEmpty == true)) {
generateProductWiseSaleHistoryReportPdf(context, tx, business, fromDate, toDate);
} else {
EasyLoading.showError(_lang.listIsEmpty);
}
},
icon: HugeIcon(icon: HugeIcons.strokeRoundedPdf02, color: kSecondayColor),
),
/*
IconButton(
onPressed: () {
if (!permissionService.hasPermission(Permit.lossProfitsRead.value)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text('You do not have permission of loss profit.'),
),
);
return;
}
if ((tx.data?.isNotEmpty == true) || (tx.data?.isNotEmpty == true)) {
// generateLossProfitReportExcel(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showError('List is empty');
}
},
icon: SvgPicture.asset('assets/excel.svg'),
),
*/
SizedBox(width: 8),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(50),
child: Column(
children: [
Divider(thickness: 1, color: kBottomBorder, height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
flex: 2,
child: Row(
children: [
Icon(IconlyLight.calendar, color: kPeraColor, size: 20),
SizedBox(width: 3),
GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: true);
}
},
child: Text(
fromDate != null ? DateFormat('dd MMM yyyy').format(fromDate!) : _lang.from,
style: Theme.of(context).textTheme.bodyMedium,
),
),
SizedBox(width: 4),
Text(
_lang.to,
style: _theme.textTheme.titleSmall,
),
SizedBox(width: 4),
Flexible(
child: GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: false);
}
},
child: Text(
toDate != null ? DateFormat('dd MMM yyyy').format(toDate!) : _lang.to,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
],
),
),
SizedBox(width: 2),
RotatedBox(
quarterTurns: 1,
child: Container(
height: 1,
width: 20,
color: kSubPeraColor,
),
),
SizedBox(width: 2),
Expanded(
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
iconSize: 20,
value: selectedTime,
isExpanded: true,
items: dateOptions.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(
entry.value,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium,
),
);
}).toList(),
onChanged: (value) {
if (value == null) return;
setState(() {
selectedTime = value;
_showCustomDatePickers = value == 'custom_date';
});
if (value != 'custom_date') {
_setDateRangeFromDropdown(value);
_refreshFilteredProvider();
}
},
),
),
),
],
),
),
Divider(thickness: 1, color: kBottomBorder, height: 1),
],
),
),
iconTheme: const IconThemeData(color: Colors.black),
centerTitle: true,
elevation: 0.0,
),
body: RefreshIndicator(
onRefresh: _refreshFilteredProvider,
child: Column(
children: [
DefaultTextStyle.merge(
style: _theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 15,
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xffF7F7F7),
border: Border(bottom: Divider.createBorderSide(context)),
),
child: Row(
children: [
Expanded(
flex: 3,
child: Text(_lang.name, textAlign: TextAlign.start),
),
Expanded(
flex: 3,
child: Text(_lang.purchase, textAlign: TextAlign.center),
),
Expanded(
flex: 2,
child: Text(
_lang.sold,
textAlign: TextAlign.center,
),
),
Expanded(
flex: 3,
child: Text(
_lang.remaining,
textAlign: TextAlign.end,
),
),
],
),
),
),
Expanded(
child: ListView.builder(
itemCount: _items.length,
itemBuilder: (context, index) {
final _item = _items[index];
return GestureDetector(
onTap: () async {
return Navigator.of(context).push<void>(MaterialPageRoute(
builder: (_) => ProductSaleHistoryReportDetails(data: _item),
));
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 6,
),
decoration: BoxDecoration(
border: Border(
bottom: Divider.createBorderSide(context),
),
),
child: Row(
children: [
Expanded(
flex: 3,
child: Text.rich(
TextSpan(
text: "${_item.name ?? "N/A"}\n",
children: [
TextSpan(
text: '${_lang.price}: ',
children: [
TextSpan(
text: "$currency${formatPointNumber(_item.salePrice ?? 0)}",
style:
TextStyle(fontWeight: FontWeight.w500, color: Colors.black),
)
],
style: TextStyle(
fontWeight: FontWeight.normal,
color: const Color(0xff4B5563),
),
),
],
),
style: _theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
),
Expanded(
flex: 3,
child: Text(
(_item.purchaseQuantity ?? 0).toString(),
textAlign: TextAlign.center,
),
),
Expanded(
flex: 2,
child: Text(
(_item.saleQuantity ?? 0).toString(),
textAlign: TextAlign.center,
),
),
Expanded(
flex: 3,
child: Text(
'$currency${(_item.remainingQuantity ?? 0).toString()}',
textAlign: TextAlign.end,
),
),
],
),
),
);
},
),
)
],
),
),
bottomNavigationBar: DefaultTextStyle.merge(
style: _theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: kTextColor,
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
border: Border(top: Divider.createBorderSide(context)),
),
child: Row(
children: [
Expanded(
flex: 3,
child: Text(_lang.total, textAlign: TextAlign.start),
),
Expanded(
flex: 2,
child: Text("${tx.totalPurchaseQuantity ?? 0}", textAlign: TextAlign.center),
),
Expanded(
flex: 2,
child: Text(
"${tx.totalSaleQuantity ?? 0}",
textAlign: TextAlign.center,
),
),
Expanded(
flex: 2,
child: Text(
"$currency${tx.totalRemainingQuantity}",
textAlign: TextAlign.end,
style: TextStyle(color: DAppColors.kError),
),
),
],
),
),
),
),
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: () => Center(child: CircularProgressIndicator()),
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: () => Center(child: CircularProgressIndicator()),
);
},
);
}
}

View File

@@ -0,0 +1,735 @@
import 'package:dropdown_button2/dropdown_button2.dart';
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:iconly/iconly.dart';
import 'package:intl/intl.dart';
import 'package:mobile_pos/Provider/transactions_provider.dart';
import 'package:mobile_pos/generated/l10n.dart' as l;
import 'package:mobile_pos/pdf_report/purchase_report/purchase_report_pdf.dart';
import 'package:mobile_pos/pdf_report/purchase_report/purchase_report_excel.dart';
import 'package:nb_utils/nb_utils.dart';
import '../../../GlobalComponents/glonal_popup.dart';
import '../../../GlobalComponents/returned_tag_widget.dart';
import '../../../PDF Invoice/purchase_invoice_pdf.dart';
import '../../../Provider/profile_provider.dart';
import '../../../constant.dart';
import '../../../core/theme/_app_colors.dart';
import '../../../currency.dart';
import '../../../http_client/custome_http_client.dart';
import '../../../thermal priting invoices/model/print_transaction_model.dart';
import '../../../thermal priting invoices/provider/print_thermal_invoice_provider.dart';
import '../../../widgets/empty_widget/_empty_widget.dart';
import '../../../service/check_user_role_permission_provider.dart';
import '../../invoice_details/purchase_invoice_details.dart';
class PurchaseReportScreen extends ConsumerStatefulWidget {
const PurchaseReportScreen({super.key});
@override
PurchaseReportState createState() => PurchaseReportState();
}
class PurchaseReportState extends ConsumerState<PurchaseReportScreen> {
final TextEditingController fromDateController = TextEditingController();
final TextEditingController toDateController = TextEditingController();
final Map<String, String> dateOptions = {
'today': l.S.current.today,
'yesterday': l.S.current.yesterday,
'last_seven_days': l.S.current.last7Days,
'last_thirty_days': l.S.current.last30Days,
'current_month': l.S.current.currentMonth,
'last_month': l.S.current.lastMonth,
'current_year': l.S.current.currentYear,
'custom_date': l.S.current.customDate,
};
String selectedTime = 'today';
bool _isRefreshing = false;
bool _showCustomDatePickers = false;
DateTime? fromDate;
DateTime? toDate;
String searchCustomer = '';
/// Generates the date range string for the provider
FilterModel _getDateRangeFilter() {
if (_showCustomDatePickers && fromDate != null && toDate != null) {
return FilterModel(
duration: 'custom_date',
fromDate: DateFormat('yyyy-MM-dd', 'en_US').format(fromDate!),
toDate: DateFormat('yyyy-MM-dd', 'en_US').format(toDate!),
);
} else {
return FilterModel(duration: selectedTime.toLowerCase());
}
}
Future<void> _selectDate({
required BuildContext context,
required bool isFrom,
}) async {
final DateTime? picked = await showDatePicker(
context: context,
firstDate: DateTime(2021),
lastDate: DateTime.now(),
initialDate: isFrom ? fromDate ?? DateTime.now() : toDate ?? DateTime.now(),
);
if (picked != null) {
setState(() {
if (isFrom) {
fromDate = picked;
fromDateController.text = DateFormat('yyyy-MM-dd').format(picked);
} else {
toDate = picked;
toDateController.text = DateFormat('yyyy-MM-dd').format(picked);
}
});
if (fromDate != null && toDate != null) _refreshFilteredProvider();
}
}
Future<void> _refreshFilteredProvider() async {
if (_isRefreshing) return;
_isRefreshing = true;
try {
final filter = _getDateRangeFilter();
ref.refresh(filterPurchaseProvider(filter));
await Future.delayed(const Duration(milliseconds: 300)); // small delay
} finally {
_isRefreshing = false;
}
}
@override
void dispose() {
fromDateController.dispose();
toDateController.dispose();
super.dispose();
}
void _updateDateUI(DateTime? from, DateTime? to) {
setState(() {
fromDate = from;
toDate = to;
fromDateController.text = from != null ? DateFormat('yyyy-MM-dd').format(from) : '';
toDateController.text = to != null ? DateFormat('yyyy-MM-dd').format(to) : '';
});
}
void _setDateRangeFromDropdown(String value) {
final now = DateTime.now();
switch (value) {
case 'today':
_updateDateUI(now, now);
break;
case 'yesterday':
final y = now.subtract(const Duration(days: 1));
_updateDateUI(y, y);
break;
case 'last_seven_days':
_updateDateUI(
now.subtract(const Duration(days: 6)),
now,
);
break;
case 'last_thirty_days':
_updateDateUI(
now.subtract(const Duration(days: 29)),
now,
);
break;
case 'current_month':
_updateDateUI(
DateTime(now.year, now.month, 1),
now,
);
break;
case 'last_month':
final first = DateTime(now.year, now.month - 1, 1);
final last = DateTime(now.year, now.month, 0);
_updateDateUI(first, last);
break;
case 'current_year':
_updateDateUI(
DateTime(now.year, 1, 1),
now,
);
break;
case 'custom_date':
// Custom: User will select manually
_updateDateUI(null, null);
break;
}
}
@override
void initState() {
super.initState();
final now = DateTime.now();
// Set initial From and To date = TODAY
fromDate = now;
toDate = now;
fromDateController.text = DateFormat('yyyy-MM-dd').format(now);
toDateController.text = DateFormat('yyyy-MM-dd').format(now);
}
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
return Consumer(
builder: (context, ref, __) {
final filter = _getDateRangeFilter();
final purchaseData = ref.watch(filterPurchaseProvider(filter));
final printerData = ref.watch(thermalPrinterProvider);
final personalData = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
title: Text(
l.S.of(context).purchaseReport,
),
actions: [
personalData.when(
data: (business) {
return purchaseData.when(
data: (transaction) {
return Row(
children: [
IconButton(
onPressed: () {
if (transaction.isNotEmpty) {
generatePurchaseReport(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showError(l.S.of(context).listIsEmpty);
}
},
icon: HugeIcon(icon: HugeIcons.strokeRoundedPdf02, color: kSecondayColor),
),
IconButton(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
onPressed: () {
if (transaction.isNotEmpty) {
generatePurchaseReportExcel(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showInfo(l.S.of(context).noDataAvailableForGeneratePdf);
}
},
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: const Size.fromHeight(50),
child: Column(
children: [
Divider(thickness: 1, color: kBottomBorder, height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
flex: 2,
child: Row(
children: [
Icon(IconlyLight.calendar, color: kPeraColor, size: 20),
SizedBox(width: 3),
GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: true);
}
},
child: Text(
fromDate != null
? DateFormat('dd MMM yyyy').format(fromDate!)
: l.S.of(context).from,
style: Theme.of(context).textTheme.bodyMedium,
),
),
SizedBox(width: 4),
Text(
l.S.of(context).to,
style: _theme.textTheme.titleSmall,
),
SizedBox(width: 4),
Flexible(
child: GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: false);
}
},
child: Text(
toDate != null ? DateFormat('dd MMM yyyy').format(toDate!) : l.S.of(context).to,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
],
),
),
SizedBox(width: 2),
RotatedBox(
quarterTurns: 1,
child: Container(
height: 1,
width: 20,
color: kSubPeraColor,
),
),
SizedBox(width: 2),
Expanded(
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
iconSize: 20,
value: selectedTime,
isExpanded: true,
items: dateOptions.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(
entry.value,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium,
),
);
}).toList(),
onChanged: (value) {
if (value == null) return;
setState(() {
selectedTime = value;
_showCustomDatePickers = value == 'custom_date';
});
if (value != 'custom_date') {
_setDateRangeFromDropdown(value);
_refreshFilteredProvider();
}
},
),
),
),
],
),
),
Divider(thickness: 1, color: kBottomBorder, height: 1),
],
),
),
iconTheme: const IconThemeData(color: Colors.black),
centerTitle: true,
backgroundColor: Colors.white,
elevation: 0.0,
),
body: RefreshIndicator(
onRefresh: () => _refreshFilteredProvider(),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children: [
if (permissionService.hasPermission(Permit.purchaseReportsRead.value)) ...{
Padding(
padding: const EdgeInsets.only(right: 16.0, left: 16.0, top: 12, bottom: 0),
child: TextFormField(
onChanged: (value) {
setState(() {
searchCustomer = value.toLowerCase().trim();
});
},
decoration: InputDecoration(
prefixIconConstraints: const BoxConstraints(
minHeight: 20,
minWidth: 20,
),
prefixIcon: Padding(
padding: const EdgeInsetsDirectional.only(start: 10),
child: Icon(
FeatherIcons.search,
color: kGrey6,
),
),
hintText: l.S.of(context).searchH,
),
),
),
purchaseData.when(data: (transaction) {
final filteredTransactions = transaction.where((purchase) {
final customerName = purchase.user?.name?.toLowerCase() ?? '';
final invoiceNumber = purchase.invoiceNumber?.toLowerCase() ?? '';
return customerName.contains(searchCustomer) || invoiceNumber.contains(searchCustomer);
}).toList();
final totalPurchase =
filteredTransactions.fold<num>(0, (sum, purchase) => sum + (purchase.totalAmount ?? 0));
final totalDues =
filteredTransactions.fold<num>(0, (sum, purchase) => sum + (purchase.dueAmount ?? 0));
return Column(
children: [
Padding(
padding: EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Row(
children: [
Expanded(
child: Container(
height: 77,
decoration: BoxDecoration(
color: kSuccessColor.withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(totalPurchase)}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
l.S.of(context).totalPurchase,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
),
SizedBox(width: 12),
Expanded(
child: Container(
height: 77,
width: double.infinity,
decoration: BoxDecoration(
color: DAppColors.kWarning.withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(totalDues)}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
l.S.of(context).balanceDue,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
),
],
),
),
filteredTransactions.isNotEmpty
? ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: filteredTransactions.length,
itemBuilder: (context, index) {
return Column(
children: [
InkWell(
onTap: () {
PurchaseInvoiceDetails(
businessInfo: personalData.value!,
transitionModel: filteredTransactions[index],
).launch(context);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
width: context.width(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Text(
filteredTransactions[index].party?.name ?? '',
style: _theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 15,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 4),
Text(
'#${filteredTransactions[index].invoiceNumber}',
style: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: filteredTransactions[index].dueAmount! <= 0
? const Color(0xff0dbf7d).withValues(alpha: 0.1)
: const Color(0xFFED1A3B).withValues(alpha: 0.1),
borderRadius:
const BorderRadius.all(Radius.circular(4))),
child: Text(
filteredTransactions[index].dueAmount! <= 0
? l.S.of(context).paid
: l.S.of(context).unPaid,
style: _theme.textTheme.titleSmall?.copyWith(
color: filteredTransactions[index].dueAmount! <= 0
? const Color(0xff0dbf7d)
: const Color(0xFFED1A3B),
fontWeight: FontWeight.w500,
),
),
),
///________Return_tag_________________________________________
ReturnedTagWidget(
show: filteredTransactions[index]
.purchaseReturns
?.isNotEmpty ??
false),
],
),
Text(
DateFormat.yMMMd().format(DateTime.parse(
filteredTransactions[index].purchaseDate ?? '')),
style: _theme.textTheme.bodyMedium?.copyWith(
color: kPeragrapColor,
),
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${l.S.of(context).total} : $currency ${filteredTransactions[index].totalAmount.toString()}',
style: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
const SizedBox(width: 4),
if (filteredTransactions[index].dueAmount!.toInt() != 0)
Text(
'${l.S.of(context).paid} : $currency ${filteredTransactions[index].totalAmount!.toDouble() - filteredTransactions[index].dueAmount!.toDouble()}',
style: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
const SizedBox(height: 3),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (filteredTransactions[index].dueAmount!.toInt() == 0)
Text(
'${l.S.of(context).paid} : $currency ${filteredTransactions[index].totalAmount!.toDouble() - filteredTransactions[index].dueAmount!.toDouble()}',
style: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
if (filteredTransactions[index].dueAmount!.toInt() != 0)
Text(
'${l.S.of(context).due}: $currency ${filteredTransactions[index].dueAmount.toString()}',
style: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
personalData.when(data: (data) {
return Row(
children: [
IconButton(
padding: EdgeInsets.zero,
visualDensity:
const VisualDensity(horizontal: -4, vertical: -4),
onPressed: () async {
if ((Theme.of(context).platform ==
TargetPlatform.android)) {
///________Print_______________________________________________________
PrintPurchaseTransactionModel model =
PrintPurchaseTransactionModel(
purchaseTransitionModel:
filteredTransactions[index],
personalInformationModel: data);
await printerData.printPurchaseThermalInvoiceNow(
transaction: model,
productList: model.purchaseTransitionModel!.details,
context: context,
invoiceSize: data.data?.invoiceSize,
);
}
},
icon: const Icon(
FeatherIcons.printer,
color: kPeraColor,
size: 22,
),
),
const SizedBox(width: 10),
Row(
children: [
IconButton(
padding: EdgeInsets.zero,
visualDensity:
const VisualDensity(horizontal: -4, vertical: -4),
onPressed: () =>
PurchaseInvoicePDF.generatePurchaseDocument(
filteredTransactions[index], data, context,
showPreview: true),
icon: HugeIcon(
icon: HugeIcons.strokeRoundedPdf02,
size: 22,
color: kPeraColor,
),
),
IconButton(
padding: EdgeInsets.zero,
visualDensity:
const VisualDensity(horizontal: -4, vertical: -4),
onPressed: () =>
PurchaseInvoicePDF.generatePurchaseDocument(
filteredTransactions[index], data, context,
download: true),
icon: HugeIcon(
icon: HugeIcons.strokeRoundedDownload01,
size: 22,
color: kPeraColor,
),
),
IconButton(
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
)),
onPressed: () =>
PurchaseInvoicePDF.generatePurchaseDocument(
filteredTransactions[index], data, context,
isShare: true),
icon: HugeIcon(
icon: HugeIcons.strokeRoundedShare08,
size: 22,
color: kPeraColor,
),
),
],
),
],
);
}, error: (e, stack) {
return Text(e.toString());
}, loading: () {
//return const Text('Loading');
return Text(l.S.of(context).loading);
}),
],
),
],
),
),
),
const Divider(height: 0, color: kBottomBorder),
],
);
},
)
: Center(
child: EmptyWidgetUpdated(
message: TextSpan(
text: l.S.of(context).addSale,
),
),
),
],
);
}, error: (e, stack) {
return Text(e.toString());
}, loading: () {
return const Center(child: CircularProgressIndicator());
}),
} else
Center(child: PermitDenyWidget()),
],
),
),
),
),
);
},
);
}
}

View File

@@ -0,0 +1,722 @@
import 'package:dropdown_button2/dropdown_button2.dart';
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:iconly/iconly.dart';
import 'package:intl/intl.dart';
import 'package:mobile_pos/Provider/transactions_provider.dart';
import 'package:mobile_pos/generated/l10n.dart' as l;
import 'package:mobile_pos/pdf_report/purchase_return_report/purchase_return_excel.dart';
import 'package:mobile_pos/pdf_report/purchase_return_report/purchase_returned_pdf.dart';
import 'package:nb_utils/nb_utils.dart';
import '../../../GlobalComponents/glonal_popup.dart';
import '../../../PDF Invoice/purchase_invoice_pdf.dart';
import '../../../Provider/profile_provider.dart';
import '../../../constant.dart';
import '../../../core/theme/_app_colors.dart';
import '../../../currency.dart';
import '../../../thermal priting invoices/model/print_transaction_model.dart';
import '../../../thermal priting invoices/provider/print_thermal_invoice_provider.dart';
import '../../../widgets/empty_widget/_empty_widget.dart';
import '../../../service/check_user_role_permission_provider.dart';
import '../../invoice_details/purchase_invoice_details.dart';
class PurchaseReturnReportScreen extends ConsumerStatefulWidget {
const PurchaseReturnReportScreen({super.key});
@override
PurchaseReportState createState() => PurchaseReportState();
}
class PurchaseReportState extends ConsumerState<PurchaseReturnReportScreen> {
final TextEditingController fromDateController = TextEditingController();
final TextEditingController toDateController = TextEditingController();
final Map<String, String> dateOptions = {
'today': l.S.current.today,
'yesterday': l.S.current.yesterday,
'last_seven_days': l.S.current.last7Days,
'last_thirty_days': l.S.current.last30Days,
'current_month': l.S.current.currentMonth,
'last_month': l.S.current.lastMonth,
'current_year': l.S.current.currentYear,
'custom_date': l.S.current.customerDate,
};
String selectedTime = 'today';
bool _isRefreshing = false;
bool _showCustomDatePickers = false;
DateTime? fromDate;
DateTime? toDate;
String searchCustomer = '';
/// Generates the date range string for the provider
FilterModel _getDateRangeFilter() {
if (_showCustomDatePickers && fromDate != null && toDate != null) {
return FilterModel(
duration: 'custom_date',
fromDate: DateFormat('yyyy-MM-dd', 'en_US').format(fromDate!),
toDate: DateFormat('yyyy-MM-dd', 'en_US').format(toDate!),
);
} else {
return FilterModel(duration: selectedTime.toLowerCase());
}
}
Future<void> _selectDate({
required BuildContext context,
required bool isFrom,
}) async {
final DateTime? picked = await showDatePicker(
context: context,
firstDate: DateTime(2021),
lastDate: DateTime.now(),
initialDate: isFrom ? fromDate ?? DateTime.now() : toDate ?? DateTime.now(),
);
if (picked != null) {
setState(() {
if (isFrom) {
fromDate = picked;
fromDateController.text = DateFormat('yyyy-MM-dd').format(picked);
} else {
toDate = picked;
toDateController.text = DateFormat('yyyy-MM-dd').format(picked);
}
});
if (fromDate != null && toDate != null) _refreshFilteredProvider();
}
}
Future<void> _refreshFilteredProvider() async {
if (_isRefreshing) return;
_isRefreshing = true;
try {
final filter = _getDateRangeFilter();
ref.refresh(filterPurchaseReturnProvider(filter));
await Future.delayed(const Duration(milliseconds: 300)); // small delay
} finally {
_isRefreshing = false;
}
}
@override
void dispose() {
fromDateController.dispose();
toDateController.dispose();
super.dispose();
}
void _updateDateUI(DateTime? from, DateTime? to) {
setState(() {
fromDate = from;
toDate = to;
fromDateController.text = from != null ? DateFormat('yyyy-MM-dd').format(from) : '';
toDateController.text = to != null ? DateFormat('yyyy-MM-dd').format(to) : '';
});
}
void _setDateRangeFromDropdown(String value) {
final now = DateTime.now();
switch (value) {
case 'today':
_updateDateUI(now, now);
break;
case 'yesterday':
final y = now.subtract(const Duration(days: 1));
_updateDateUI(y, y);
break;
case 'last_seven_days':
_updateDateUI(
now.subtract(const Duration(days: 6)),
now,
);
break;
case 'last_thirty_days':
_updateDateUI(
now.subtract(const Duration(days: 29)),
now,
);
break;
case 'current_month':
_updateDateUI(
DateTime(now.year, now.month, 1),
now,
);
break;
case 'last_month':
final first = DateTime(now.year, now.month - 1, 1);
final last = DateTime(now.year, now.month, 0);
_updateDateUI(first, last);
break;
case 'current_year':
_updateDateUI(
DateTime(now.year, 1, 1),
now,
);
break;
case 'custom_date':
// Custom: User will select manually
_updateDateUI(null, null);
break;
}
}
@override
void initState() {
super.initState();
final now = DateTime.now();
// Set initial From and To date = TODAY
fromDate = now;
toDate = now;
fromDateController.text = DateFormat('yyyy-MM-dd').format(now);
toDateController.text = DateFormat('yyyy-MM-dd').format(now);
}
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
return Consumer(
builder: (context, ref, __) {
final filter = _getDateRangeFilter();
final purchaseData = ref.watch(filterPurchaseReturnProvider(filter));
final printerData = ref.watch(thermalPrinterProvider);
final businessInfo = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
title: Text(
l.S.of(context).purchaseReturnReport,
),
actions: [
businessInfo.when(
data: (business) {
return purchaseData.when(
data: (transaction) {
return Row(
children: [
IconButton(
onPressed: () {
if (transaction.isNotEmpty) {
generatePurchaseReturnReportPdf(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showError(l.S.of(context).listIsEmpty);
}
},
icon: HugeIcon(icon: HugeIcons.strokeRoundedPdf02, color: kSecondayColor),
),
IconButton(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
onPressed: () {
if (transaction.isNotEmpty) {
generatePurchaseReturnReportExcel(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showInfo(l.S.of(context).noDataAvailableForGeneratePdf);
}
},
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: const Size.fromHeight(50),
child: Column(
children: [
Divider(thickness: 1, color: kBottomBorder, height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
flex: 2,
child: Row(
children: [
Icon(IconlyLight.calendar, color: kPeraColor, size: 20),
SizedBox(width: 3),
GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: true);
}
},
child: Text(
fromDate != null ? DateFormat('dd MMM yyyy').format(fromDate!) : 'From',
style: Theme.of(context).textTheme.bodyMedium,
),
),
SizedBox(width: 4),
Text(
'To',
style: _theme.textTheme.titleSmall,
),
SizedBox(width: 4),
Flexible(
child: GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: false);
}
},
child: Text(
toDate != null ? DateFormat('dd MMM yyyy').format(toDate!) : 'To',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
],
),
),
SizedBox(width: 2),
RotatedBox(
quarterTurns: 1,
child: Container(
height: 1,
width: 20,
color: kSubPeraColor,
),
),
SizedBox(width: 2),
Expanded(
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
iconSize: 20,
value: selectedTime,
isExpanded: true,
items: dateOptions.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(
entry.value,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium,
),
);
}).toList(),
onChanged: (value) {
if (value == null) return;
setState(() {
selectedTime = value;
_showCustomDatePickers = value == 'custom_date';
});
if (value != 'custom_date') {
_setDateRangeFromDropdown(value);
_refreshFilteredProvider();
}
},
),
),
),
],
),
),
Divider(thickness: 1, color: kBottomBorder, height: 1),
],
),
),
iconTheme: const IconThemeData(color: Colors.black),
centerTitle: true,
backgroundColor: Colors.white,
elevation: 0.0,
),
body: RefreshIndicator(
onRefresh: () => _refreshFilteredProvider(),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children: [
if (permissionService.hasPermission(Permit.purchaseReturnReportsRead.value)) ...{
Padding(
padding: const EdgeInsets.only(right: 16.0, left: 16.0, top: 12, bottom: 0),
child: TextFormField(
onChanged: (value) {
setState(() {
searchCustomer = value.toLowerCase().trim();
});
},
decoration: InputDecoration(
prefixIconConstraints: const BoxConstraints(
minHeight: 20,
minWidth: 20,
),
prefixIcon: Padding(
padding: const EdgeInsetsDirectional.only(start: 10),
child: Icon(
FeatherIcons.search,
color: kGrey6,
),
),
hintText: l.S.of(context).searchH,
),
),
),
purchaseData.when(data: (transaction) {
final filteredTransactions = transaction.where((sale) {
final customerName = sale.user?.name?.toLowerCase() ?? '';
final invoiceNumber = sale.invoiceNumber?.toLowerCase() ?? '';
return customerName.contains(searchCustomer) || invoiceNumber.contains(searchCustomer);
}).toList();
final totalPurchaseReturn =
filteredTransactions.fold<num>(0, (sum, purchase) => sum + (purchase.totalAmount ?? 0));
final totalDues =
filteredTransactions.fold<num>(0, (sum, purchase) => sum + (purchase.dueAmount ?? 0));
return Column(
children: [
Padding(
padding: EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Row(
children: [
Expanded(
child: Container(
height: 77,
decoration: BoxDecoration(
color: kSuccessColor.withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(totalPurchaseReturn)}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
l.S.of(context).totalPurchase,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
),
SizedBox(width: 12),
Expanded(
child: Container(
height: 77,
width: double.infinity,
decoration: BoxDecoration(
color: DAppColors.kWarning.withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(totalDues)}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
l.S.of(context).balanceDue,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
),
],
),
),
filteredTransactions.isNotEmpty
? ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: filteredTransactions.length,
itemBuilder: (context, index) {
num returndAmount = 0;
for (var element in filteredTransactions[index].purchaseReturns!) {
for (var sales in element.purchaseReturnDetails!) {
returndAmount += (sales.returnAmount ?? 0);
}
}
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: InkWell(
onTap: () {
PurchaseInvoiceDetails(
businessInfo: businessInfo.value!,
transitionModel: filteredTransactions[index],
).launch(context);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Text(
filteredTransactions[index].party?.name ?? '',
style: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 15,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 4),
Text(
'#${filteredTransactions[index].invoiceNumber}',
style: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 6),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: filteredTransactions[index].dueAmount! <= 0
? const Color(0xff0dbf7d).withValues(alpha: 0.1)
: const Color(0xFFED1A3B).withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(
Radius.circular(4),
),
),
child: Text(
transaction[index].dueAmount! <= 0
? l.S.of(context).paid
: l.S.of(context).unPaid,
style: _theme.textTheme.titleSmall?.copyWith(
color: filteredTransactions[index].dueAmount! <= 0
? const Color(0xff0dbf7d)
: const Color(0xFFED1A3B)),
),
),
],
),
Text(
DateFormat.yMMMd().format(DateTime.parse(
filteredTransactions[index].purchaseDate ?? '')),
style: _theme.textTheme.titleSmall?.copyWith(color: kPeraColor),
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${l.S.of(context).total} : $currency${filteredTransactions[index].totalAmount.toString()}',
style: _theme.textTheme.titleSmall?.copyWith(color: kPeraColor),
),
const SizedBox(width: 4),
Text(
'${l.S.of(context).paid} : $currency${filteredTransactions[index].totalAmount!.toDouble() - filteredTransactions[index].dueAmount!.toDouble()}',
style: _theme.textTheme.titleSmall?.copyWith(color: kPeraColor),
),
],
),
const SizedBox(height: 3),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Text(
'${l.S.of(context).returnAmount}: $currency$returndAmount',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.titleSmall,
),
),
businessInfo.when(data: (data) {
return Row(
children: [
IconButton(
padding: EdgeInsets.zero,
visualDensity:
const VisualDensity(horizontal: -4, vertical: -4),
onPressed: () async {
if ((Theme.of(context).platform ==
TargetPlatform.android)) {
///________Print_______________________________________________________
PrintPurchaseTransactionModel model =
PrintPurchaseTransactionModel(
purchaseTransitionModel:
filteredTransactions[index],
personalInformationModel: data);
await printerData.printPurchaseThermalInvoiceNow(
transaction: model,
productList:
model.purchaseTransitionModel!.details,
context: context,
invoiceSize:
businessInfo.value?.data?.invoiceSize,
);
}
},
icon: const Icon(
FeatherIcons.printer,
color: kPeraColor,
size: 22,
)),
const SizedBox(
width: 10,
),
Row(
children: [
IconButton(
padding: EdgeInsets.zero,
visualDensity:
const VisualDensity(horizontal: -4, vertical: -4),
onPressed: () =>
PurchaseInvoicePDF.generatePurchaseDocument(
filteredTransactions[index], data, context,
showPreview: true),
icon: HugeIcon(
icon: HugeIcons.strokeRoundedPdf02,
size: 22,
color: kPeraColor,
),
),
IconButton(
padding: EdgeInsets.zero,
visualDensity:
const VisualDensity(horizontal: -4, vertical: -4),
onPressed: () =>
PurchaseInvoicePDF.generatePurchaseDocument(
filteredTransactions[index], data, context,
download: true),
icon: HugeIcon(
icon: HugeIcons.strokeRoundedDownload01,
size: 22,
color: kPeraColor,
),
),
IconButton(
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
)),
onPressed: () =>
PurchaseInvoicePDF.generatePurchaseDocument(
filteredTransactions[index], data, context,
isShare: true),
icon: HugeIcon(
icon: HugeIcons.strokeRoundedShare08,
size: 22,
color: kPeraColor,
),
),
],
),
],
);
}, error: (e, stack) {
return Text(e.toString());
}, loading: () {
//return const Text('Loading');
return Text(l.S.of(context).loading);
}),
],
),
],
),
),
),
Divider(
height: 0,
color: kBottomBorder,
),
],
);
},
)
: Center(
child: EmptyWidgetUpdated(
message: TextSpan(
text: l.S.of(context).addNewPurchase,
),
),
),
],
);
}, error: (e, stack) {
return Text(e.toString());
}, loading: () {
return const Center(child: CircularProgressIndicator());
}),
} else
Center(child: PermitDenyWidget()),
],
),
),
),
),
);
},
);
}
}

View File

@@ -0,0 +1,513 @@
import 'package:dropdown_button2/dropdown_button2.dart';
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:iconly/iconly.dart';
import 'package:intl/intl.dart';
import 'package:mobile_pos/Provider/transactions_provider.dart';
import 'package:mobile_pos/core/theme/_app_colors.dart';
import 'package:mobile_pos/generated/l10n.dart' as l;
import 'package:mobile_pos/pdf_report/sales_report/sales_report_excel.dart';
import 'package:mobile_pos/pdf_report/sales_report/sales_report_pdf.dart';
import 'package:nb_utils/nb_utils.dart';
import '../../../GlobalComponents/glonal_popup.dart';
import '../../../GlobalComponents/sales_transaction_widget.dart';
import '../../../Provider/profile_provider.dart';
import '../../../constant.dart';
import '../../../currency.dart';
import '../../../widgets/build_date_selector/build_date_selector.dart';
import '../../../widgets/empty_widget/_empty_widget.dart';
import '../../../service/check_user_role_permission_provider.dart';
class SalesReportScreen extends ConsumerStatefulWidget {
const SalesReportScreen({super.key});
@override
SalesReportScreenState createState() => SalesReportScreenState();
}
class SalesReportScreenState extends ConsumerState<SalesReportScreen> {
final TextEditingController fromDateController = TextEditingController();
final TextEditingController toDateController = TextEditingController();
final Map<String, String> dateOptions = {
'today': l.S.current.today,
'yesterday': l.S.current.yesterday,
'last_seven_days': l.S.current.last7Days,
'last_thirty_days': l.S.current.last30Days,
'current_month': l.S.current.currentMonth,
'last_month': l.S.current.lastMonth,
'current_year': l.S.current.currentYear,
'custom_date': l.S.current.customDate,
};
String selectedTime = 'today';
bool _isRefreshing = false;
bool _showCustomDatePickers = false;
DateTime? fromDate;
DateTime? toDate;
String searchCustomer = '';
/// Generates the date range string for the provider
FilterModel _getDateRangeFilter() {
if (_showCustomDatePickers && fromDate != null && toDate != null) {
return FilterModel(
duration: 'custom_date',
fromDate: DateFormat('yyyy-MM-dd', 'en_US').format(fromDate!),
toDate: DateFormat('yyyy-MM-dd', 'en_US').format(toDate!),
);
} else {
return FilterModel(duration: selectedTime.toLowerCase());
}
}
Future<void> _selectDate({
required BuildContext context,
required bool isFrom,
}) async {
final DateTime? picked = await showDatePicker(
context: context,
firstDate: DateTime(2021),
lastDate: DateTime.now(),
initialDate: isFrom ? fromDate ?? DateTime.now() : toDate ?? DateTime.now(),
);
if (picked != null) {
setState(() {
if (isFrom) {
fromDate = picked;
fromDateController.text = DateFormat('yyyy-MM-dd').format(picked);
} else {
toDate = picked;
toDateController.text = DateFormat('yyyy-MM-dd').format(picked);
}
});
if (fromDate != null && toDate != null) _refreshFilteredProvider();
}
}
Future<void> _refreshFilteredProvider() async {
if (_isRefreshing) return;
_isRefreshing = true;
try {
final filter = _getDateRangeFilter();
ref.refresh(filteredSaleProvider(filter));
await Future.delayed(const Duration(milliseconds: 300)); // small delay
} finally {
_isRefreshing = false;
}
}
@override
void dispose() {
fromDateController.dispose();
toDateController.dispose();
super.dispose();
}
void _updateDateUI(DateTime? from, DateTime? to) {
setState(() {
fromDate = from;
toDate = to;
fromDateController.text = from != null ? DateFormat('yyyy-MM-dd').format(from) : '';
toDateController.text = to != null ? DateFormat('yyyy-MM-dd').format(to) : '';
});
}
void _setDateRangeFromDropdown(String value) {
final now = DateTime.now();
switch (value) {
case 'today':
_updateDateUI(now, now);
break;
case 'yesterday':
final y = now.subtract(const Duration(days: 1));
_updateDateUI(y, y);
break;
case 'last_seven_days':
_updateDateUI(
now.subtract(const Duration(days: 6)),
now,
);
break;
case 'last_thirty_days':
_updateDateUI(
now.subtract(const Duration(days: 29)),
now,
);
break;
case 'current_month':
_updateDateUI(
DateTime(now.year, now.month, 1),
now,
);
break;
case 'last_month':
final first = DateTime(now.year, now.month - 1, 1);
final last = DateTime(now.year, now.month, 0);
_updateDateUI(first, last);
break;
case 'current_year':
_updateDateUI(
DateTime(now.year, 1, 1),
now,
);
break;
case 'custom_date':
// Custom: User will select manually
_updateDateUI(null, null);
break;
}
}
@override
void initState() {
super.initState();
final now = DateTime.now();
// Set initial From and To date = TODAY
fromDate = now;
toDate = now;
fromDateController.text = DateFormat('yyyy-MM-dd').format(now);
toDateController.text = DateFormat('yyyy-MM-dd').format(now);
}
@override
Widget build(BuildContext context) {
final _lang = l.S.of(context);
final _theme = Theme.of(context);
return Consumer(
builder: (context, ref, __) {
final filter = _getDateRangeFilter();
final providerData = ref.watch(filteredSaleProvider(filter));
final personalData = ref.watch(businessInfoProvider);
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
title: Text(
l.S.of(context).salesReport,
),
actions: [
personalData.when(
data: (business) {
return providerData.when(
data: (transaction) {
return Row(
children: [
IconButton(
onPressed: () {
if (transaction.isNotEmpty) {
generateSaleReportPdf(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showError(_lang.listIsEmpty);
}
},
icon: HugeIcon(icon: HugeIcons.strokeRoundedPdf02, color: kSecondayColor),
),
IconButton(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
onPressed: () {
if (transaction.isNotEmpty) {
generateSaleReportExcel(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showInfo(_lang.noDataAvailableForGeneratePdf);
}
},
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: const Size.fromHeight(50),
child: Column(
children: [
Divider(thickness: 1, color: kBottomBorder, height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
flex: 2,
child: Row(
children: [
Icon(IconlyLight.calendar, color: kPeraColor, size: 20),
SizedBox(width: 3),
GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: true);
}
},
child: Text(
fromDate != null ? DateFormat('dd MMM yyyy').format(fromDate!) : 'From',
style: Theme.of(context).textTheme.bodyMedium,
),
),
SizedBox(width: 4),
Text(
_lang.to,
style: _theme.textTheme.titleSmall,
),
SizedBox(width: 4),
Flexible(
child: GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: false);
}
},
child: Text(
toDate != null ? DateFormat('dd MMM yyyy').format(toDate!) : 'To',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
],
),
),
SizedBox(width: 2),
RotatedBox(
quarterTurns: 1,
child: Container(
height: 1,
width: 20,
color: kSubPeraColor,
),
),
SizedBox(width: 2),
Expanded(
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
iconSize: 20,
value: selectedTime,
isExpanded: true,
items: dateOptions.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(
entry.value,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium,
),
);
}).toList(),
onChanged: (value) {
if (value == null) return;
setState(() {
selectedTime = value;
_showCustomDatePickers = value == 'custom_date';
});
if (value != 'custom_date') {
_setDateRangeFromDropdown(value);
_refreshFilteredProvider();
}
},
),
),
),
],
),
),
Divider(thickness: 1, color: kBottomBorder, height: 1),
],
),
),
iconTheme: const IconThemeData(color: Colors.black),
centerTitle: true,
backgroundColor: Colors.white,
elevation: 0.0,
),
body: RefreshIndicator(
onRefresh: () => _refreshFilteredProvider(),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(right: 16.0, left: 16.0, top: 12, bottom: 0),
child: Column(
children: [
TextFormField(
onChanged: (value) {
setState(() {
searchCustomer = value.toLowerCase().trim();
});
},
decoration: InputDecoration(
prefixIconConstraints: const BoxConstraints(
minHeight: 20,
minWidth: 20,
),
prefixIcon: Padding(
padding: const EdgeInsetsDirectional.only(start: 10),
child: Icon(
FeatherIcons.search,
color: kGrey6,
),
),
hintText: l.S.of(context).searchH,
),
),
],
),
),
providerData.when(data: (transaction) {
final filteredTransactions = transaction.where((sale) {
final customerName = sale.user?.name?.toLowerCase() ?? '';
final invoiceNumber = sale.invoiceNumber?.toLowerCase() ?? '';
return customerName.contains(searchCustomer) || invoiceNumber.contains(searchCustomer);
}).toList();
final totalSales =
filteredTransactions.fold<num>(0, (sum, sale) => sum + (sale.totalAmount ?? 0));
final totalDue = filteredTransactions.fold<num>(0, (sum, due) => sum + (due.dueAmount ?? 0));
return filteredTransactions.isNotEmpty
? Column(
children: [
Padding(
padding: EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Row(
children: [
Expanded(
child: Container(
height: 77,
decoration: BoxDecoration(
color: kSuccessColor.withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(totalSales)}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
l.S.of(context).totalSales,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
),
SizedBox(width: 12),
Expanded(
child: Container(
height: 77,
width: double.infinity,
decoration: BoxDecoration(
color: DAppColors.kWarning.withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(totalDue)}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
_lang.balanceDue,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
),
],
),
),
ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: filteredTransactions.length,
itemBuilder: (context, index) {
return salesTransactionWidget(
context: context,
ref: ref,
businessInfo: personalData.value!,
sale: filteredTransactions[index],
advancePermission: true,
);
},
)
],
)
: Center(
child: EmptyWidgetUpdated(
message: TextSpan(
text: l.S.of(context).addSale,
),
),
);
}, error: (e, stack) {
return Text(e.toString());
}, loading: () {
print('-------print again and again------------');
return const Center(child: CircularProgressIndicator());
}),
],
),
),
),
),
);
},
);
}
}

View File

@@ -0,0 +1,547 @@
import 'dart:io';
import 'package:dropdown_button2/dropdown_button2.dart';
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:iconly/iconly.dart';
import 'package:intl/intl.dart';
import 'package:mobile_pos/Provider/transactions_provider.dart';
import 'package:mobile_pos/generated/l10n.dart' as l;
import 'package:nb_utils/nb_utils.dart';
import '../../../GlobalComponents/glonal_popup.dart';
import '../../../GlobalComponents/sales_transaction_widget.dart';
import '../../../Provider/profile_provider.dart';
import '../../../constant.dart';
import '../../../core/theme/_app_colors.dart';
import '../../../currency.dart';
import '../../../pdf_report/sales_report/sales_report_excel.dart';
import '../../../pdf_report/sales_report/sales_report_pdf.dart';
import '../../../pdf_report/sales_retunrn_report/sales_returned_excel.dart';
import '../../../pdf_report/sales_retunrn_report/sales_returned_pdf.dart';
import '../../../thermal priting invoices/provider/print_thermal_invoice_provider.dart';
import '../../../service/check_user_role_permission_provider.dart';
import '../../../widgets/empty_widget/_empty_widget.dart';
class SalesReturnReportScreen extends ConsumerStatefulWidget {
const SalesReturnReportScreen({super.key});
@override
SalesReturnReportScreenState createState() => SalesReturnReportScreenState();
}
class SalesReturnReportScreenState extends ConsumerState<SalesReturnReportScreen> {
final TextEditingController fromDateController = TextEditingController();
final TextEditingController toDateController = TextEditingController();
final Map<String, String> dateOptions = {
'today': l.S.current.today,
'yesterday': l.S.current.yesterday,
'last_seven_days': l.S.current.last7Days,
'last_thirty_days': l.S.current.last30Days,
'current_month': l.S.current.currentMonth,
'last_month': l.S.current.lastMonth,
'current_year': l.S.current.currentYear,
'custom_date': l.S.current.customerDate,
};
String selectedTime = 'today';
bool _isRefreshing = false;
bool _showCustomDatePickers = false;
DateTime? fromDate;
DateTime? toDate;
String searchCustomer = '';
/// Generates the date range string for the provider
FilterModel _getDateRangeFilter() {
if (_showCustomDatePickers && fromDate != null && toDate != null) {
return FilterModel(
duration: 'custom_date',
fromDate: DateFormat('yyyy-MM-dd', 'en_US').format(fromDate!),
toDate: DateFormat('yyyy-MM-dd', 'en_US').format(toDate!),
);
} else {
return FilterModel(duration: selectedTime.toLowerCase());
}
}
Future<void> _selectDate({
required BuildContext context,
required bool isFrom,
}) async {
final DateTime? picked = await showDatePicker(
context: context,
firstDate: DateTime(2021),
lastDate: DateTime.now(),
initialDate: isFrom ? fromDate ?? DateTime.now() : toDate ?? DateTime.now(),
);
if (picked != null) {
setState(() {
if (isFrom) {
fromDate = picked;
fromDateController.text = DateFormat('yyyy-MM-dd').format(picked);
} else {
toDate = picked;
toDateController.text = DateFormat('yyyy-MM-dd').format(picked);
}
});
if (fromDate != null && toDate != null) _refreshFilteredProvider();
}
}
Future<void> _refreshFilteredProvider() async {
if (_isRefreshing) return;
_isRefreshing = true;
try {
final filter = _getDateRangeFilter();
ref.refresh(filteredSaleReturnedProvider(filter));
await Future.delayed(const Duration(milliseconds: 300)); // small delay
} finally {
_isRefreshing = false;
}
}
void _updateDateUI(DateTime? from, DateTime? to) {
setState(() {
fromDate = from;
toDate = to;
fromDateController.text = from != null ? DateFormat('yyyy-MM-dd').format(from) : '';
toDateController.text = to != null ? DateFormat('yyyy-MM-dd').format(to) : '';
});
}
void _setDateRangeFromDropdown(String value) {
final now = DateTime.now();
switch (value) {
case 'today':
_updateDateUI(now, now);
break;
case 'yesterday':
final y = now.subtract(const Duration(days: 1));
_updateDateUI(y, y);
break;
case 'last_seven_days':
_updateDateUI(
now.subtract(const Duration(days: 6)),
now,
);
break;
case 'last_thirty_days':
_updateDateUI(
now.subtract(const Duration(days: 29)),
now,
);
break;
case 'current_month':
_updateDateUI(
DateTime(now.year, now.month, 1),
now,
);
break;
case 'last_month':
final first = DateTime(now.year, now.month - 1, 1);
final last = DateTime(now.year, now.month, 0);
_updateDateUI(first, last);
break;
case 'current_year':
_updateDateUI(
DateTime(now.year, 1, 1),
now,
);
break;
case 'custom_date':
// Custom: User will select manually
_updateDateUI(null, null);
break;
}
}
@override
void initState() {
super.initState();
final now = DateTime.now();
// Set initial From and To date = TODAY
fromDate = now;
toDate = now;
fromDateController.text = DateFormat('yyyy-MM-dd').format(now);
toDateController.text = DateFormat('yyyy-MM-dd').format(now);
}
@override
void dispose() {
fromDateController.dispose();
toDateController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
return Consumer(
builder: (context, ref, __) {
final filter = _getDateRangeFilter();
final _lang = l.S.of(context);
final providerData = ref.watch(filteredSaleReturnedProvider(filter));
final personalData = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
title: Text(
l.S.of(context).salesReturnReport,
),
actions: [
personalData.when(
data: (business) {
return providerData.when(
data: (transaction) {
return Row(
children: [
IconButton(
onPressed: () {
if (transaction.isNotEmpty) {
generateSaleReturnReportPdf(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showError(_lang.listIsEmpty);
}
},
icon: HugeIcon(icon: HugeIcons.strokeRoundedPdf02, color: kSecondayColor),
),
IconButton(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
onPressed: () {
if (transaction.isNotEmpty) {
generateSaleReturnReportExcel(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showInfo(_lang.noDataAvailableForGeneratePdf);
}
},
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: const Size.fromHeight(50),
child: Column(
children: [
Divider(thickness: 1, color: kBottomBorder, height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
flex: 2,
child: Row(
children: [
Icon(IconlyLight.calendar, color: kPeraColor, size: 20),
SizedBox(width: 3),
GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: true);
}
},
child: Text(
fromDate != null ? DateFormat('dd MMM yyyy').format(fromDate!) : _lang.from,
style: Theme.of(context).textTheme.bodyMedium,
),
),
SizedBox(width: 4),
Text(
_lang.to,
style: _theme.textTheme.titleSmall,
),
SizedBox(width: 4),
Flexible(
child: GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: false);
}
},
child: Text(
toDate != null ? DateFormat('dd MMM yyyy').format(toDate!) : _lang.to,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
],
),
),
SizedBox(width: 2),
RotatedBox(
quarterTurns: 1,
child: Container(
height: 1,
width: 20,
color: kSubPeraColor,
),
),
SizedBox(width: 2),
Expanded(
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
iconSize: 20,
value: selectedTime,
isExpanded: true,
items: dateOptions.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(
entry.value,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium,
),
);
}).toList(),
onChanged: (value) {
if (value == null) return;
setState(() {
selectedTime = value;
_showCustomDatePickers = value == 'custom_date';
});
if (value != 'custom_date') {
_setDateRangeFromDropdown(value);
_refreshFilteredProvider();
}
},
),
),
),
],
),
),
Divider(thickness: 1, color: kBottomBorder, height: 1),
],
),
),
iconTheme: const IconThemeData(color: Colors.black),
centerTitle: true,
backgroundColor: Colors.white,
elevation: 0.0,
),
body: RefreshIndicator(
onRefresh: () => _refreshFilteredProvider(),
child: Consumer(builder: (context, ref, __) {
final filter = _getDateRangeFilter();
final providerData = ref.watch(filteredSaleReturnedProvider(filter));
final printerData = ref.watch(thermalPrinterProvider);
final personalData = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children: [
providerData.when(data: (transaction) {
final filteredTransactions = transaction.where((sale) {
final customerName = sale.user?.name?.toLowerCase() ?? '';
final invoiceNumber = sale.invoiceNumber?.toLowerCase() ?? '';
return customerName.contains(searchCustomer) || invoiceNumber.contains(searchCustomer);
}).toList();
final totalSales =
filteredTransactions.fold<num>(0, (sum, saleReturn) => sum + (saleReturn.totalAmount ?? 0));
num returnAmount = 0;
for (var sale in filteredTransactions) {
for (var salesReturn in sale.salesReturns!) {
for (var sales in salesReturn.salesReturnDetails!) {
returnAmount += sales.returnAmount!;
}
}
}
return Column(
children: [
Padding(
padding: const EdgeInsets.only(right: 16.0, left: 16.0, top: 12, bottom: 0),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: TextFormField(
onChanged: (value) {
setState(() {
searchCustomer = value.toLowerCase().trim();
});
},
decoration: InputDecoration(
prefixIconConstraints: const BoxConstraints(
minHeight: 20,
minWidth: 20,
),
prefixIcon: Padding(
padding: const EdgeInsetsDirectional.only(start: 10),
child: Icon(
FeatherIcons.search,
color: kGrey6,
),
),
hintText: l.S.of(context).searchH,
),
),
),
if (_showCustomDatePickers) SizedBox(height: 10),
// const SizedBox(height: 12),
],
),
),
filteredTransactions.isNotEmpty
? Column(
children: [
Padding(
padding: EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Row(
children: [
Expanded(
child: Container(
height: 77,
decoration: BoxDecoration(
color: kSuccessColor.withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(totalSales)}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
l.S.of(context).totalSales,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
),
SizedBox(width: 12),
Expanded(
child: Container(
height: 77,
width: double.infinity,
decoration: BoxDecoration(
color: DAppColors.kWarning.withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(returnAmount)}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
_lang.returnedAmount,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
),
],
),
),
ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: transaction.length,
itemBuilder: (context, index) {
num returndAmount = 0;
for (var element in transaction[index].salesReturns!) {
for (var sales in element.salesReturnDetails!) {
returndAmount += (sales.returnAmount ?? 0);
}
}
return salesTransactionWidget(
context: context,
ref: ref,
businessInfo: personalData.value!,
sale: transaction[index],
advancePermission: true,
returnAmount: returndAmount);
},
)
],
)
: Center(
child: EmptyWidgetUpdated(
message: TextSpan(
text: _lang.pleaseAddASalesReturn,
),
),
),
],
);
}, error: (e, stack) {
return Text(e.toString());
}, loading: () {
return const Center(child: CircularProgressIndicator());
}),
],
),
);
}),
),
),
);
},
);
}
}

View File

@@ -0,0 +1,552 @@
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:iconly/iconly.dart';
import 'package:intl/intl.dart';
import 'package:mobile_pos/core/theme/_app_colors.dart';
import 'package:mobile_pos/generated/l10n.dart' as l;
import 'package:mobile_pos/Provider/transactions_provider.dart';
import 'package:mobile_pos/model/dashboard_overview_model.dart';
import '../../../PDF Invoice/subscription_invoice_pdf.dart';
import '../../../Provider/profile_provider.dart';
import '../../../constant.dart';
import '../../../GlobalComponents/glonal_popup.dart';
import '../../../pdf_report/transactions/subscription_report_pdf.dart';
import '../../../service/check_user_role_permission_provider.dart';
class SubscriptionReportScreen extends ConsumerStatefulWidget {
const SubscriptionReportScreen({super.key});
@override
ConsumerState<SubscriptionReportScreen> createState() => _SubscriptionReportScreenState();
}
class _SubscriptionReportScreenState extends ConsumerState<SubscriptionReportScreen> {
final TextEditingController fromDateController = TextEditingController();
final TextEditingController toDateController = TextEditingController();
final Map<String, String> dateOptions = {
'today': l.S.current.today,
'yesterday': l.S.current.yesterday,
'last_seven_days': l.S.current.last7Days,
'last_thirty_days': l.S.current.last30Days,
'current_month': l.S.current.currentMonth,
'last_month': l.S.current.lastMonth,
'current_year': l.S.current.currentYear,
'custom_date': l.S.current.customerDate,
};
String selectedTime = 'today';
bool _isRefreshing = false;
bool _showCustomDatePickers = false;
DateTime? fromDate;
DateTime? toDate;
String searchCustomer = '';
/// Generates the date range string for the provider
FilterModel _getDateRangeFilter() {
if (_showCustomDatePickers && fromDate != null && toDate != null) {
return FilterModel(
duration: 'custom_date',
fromDate: DateFormat('yyyy-MM-dd', 'en_US').format(fromDate!),
toDate: DateFormat('yyyy-MM-dd', 'en_US').format(toDate!),
);
} else {
return FilterModel(duration: selectedTime.toLowerCase());
}
}
Future<void> _selectDate({
required BuildContext context,
required bool isFrom,
}) async {
final DateTime? picked = await showDatePicker(
context: context,
firstDate: DateTime(2021),
lastDate: DateTime.now(),
initialDate: isFrom ? fromDate ?? DateTime.now() : toDate ?? DateTime.now(),
);
if (picked != null) {
setState(() {
if (isFrom) {
fromDate = picked;
fromDateController.text = DateFormat('yyyy-MM-dd').format(picked);
} else {
toDate = picked;
toDateController.text = DateFormat('yyyy-MM-dd').format(picked);
}
});
if (fromDate != null && toDate != null) _refreshFilteredProvider();
}
}
Future<void> _refreshFilteredProvider() async {
if (_isRefreshing) return;
_isRefreshing = true;
try {
final filter = _getDateRangeFilter();
ref.refresh(filteredSubscriptionReportProvider(filter));
await Future.delayed(const Duration(milliseconds: 300)); // small delay
} finally {
_isRefreshing = false;
}
}
@override
void dispose() {
fromDateController.dispose();
toDateController.dispose();
super.dispose();
}
void _updateDateUI(DateTime? from, DateTime? to) {
setState(() {
fromDate = from;
toDate = to;
fromDateController.text = from != null ? DateFormat('yyyy-MM-dd').format(from) : '';
toDateController.text = to != null ? DateFormat('yyyy-MM-dd').format(to) : '';
});
}
void _setDateRangeFromDropdown(String value) {
final now = DateTime.now();
switch (value) {
case 'today':
_updateDateUI(now, now);
break;
case 'yesterday':
final y = now.subtract(const Duration(days: 1));
_updateDateUI(y, y);
break;
case 'last_seven_days':
_updateDateUI(
now.subtract(const Duration(days: 6)),
now,
);
break;
case 'last_thirty_days':
_updateDateUI(
now.subtract(const Duration(days: 29)),
now,
);
break;
case 'current_month':
_updateDateUI(
DateTime(now.year, now.month, 1),
now,
);
break;
case 'last_month':
final first = DateTime(now.year, now.month - 1, 1);
final last = DateTime(now.year, now.month, 0);
_updateDateUI(first, last);
break;
case 'current_year':
_updateDateUI(
DateTime(now.year, 1, 1),
now,
);
break;
case 'custom_date':
// Custom: User will select manually
_updateDateUI(null, null);
break;
}
}
@override
void initState() {
super.initState();
final now = DateTime.now();
// Set initial From and To date = TODAY
fromDate = now;
toDate = now;
fromDateController.text = DateFormat('yyyy-MM-dd').format(now);
toDateController.text = DateFormat('yyyy-MM-dd').format(now);
}
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
final _lang = l.S.of(context);
return Consumer(
builder: (_, ref, watch) {
final providerData = ref.watch(filteredSubscriptionReportProvider(_getDateRangeFilter()));
final personalData = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
return personalData.when(
data: (business) {
return providerData.when(
data: (transaction) {
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
backgroundColor: Colors.white,
title: Text(_lang.subscriptionReports),
actions: [
IconButton(
onPressed: () {
if (transaction.isNotEmpty == true) {
generateSubscriptionReportPdf(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showError(_lang.listIsEmpty);
}
},
icon: HugeIcon(icon: HugeIcons.strokeRoundedPdf02, color: kSecondayColor),
),
/*
IconButton(
onPressed: () {
if (!permissionService.hasPermission(Permit.lossProfitsRead.value)) {
// TODO: Shakil fix this permission
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text('You do not have permission of loss profit.'),
),
);
return;
}
if ((transaction.expenseSummary?.isNotEmpty == true) ||
(transaction.incomeSummary?.isNotEmpty == true)) {
generateLossProfitReportExcel(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showError('List is empty');
}
},
icon: SvgPicture.asset('assets/excel.svg'),
),
*/
SizedBox(width: 8),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(50),
child: Column(
children: [
Divider(thickness: 1, color: kBottomBorder, height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
flex: 2,
child: Row(
children: [
Icon(IconlyLight.calendar, color: kPeraColor, size: 20),
SizedBox(width: 3),
GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: true);
}
},
child: Text(
fromDate != null ? DateFormat('dd MMM yyyy').format(fromDate!) : _lang.from,
style: Theme.of(context).textTheme.bodyMedium,
),
),
SizedBox(width: 4),
Text(
'To',
style: _theme.textTheme.titleSmall,
),
SizedBox(width: 4),
Flexible(
child: GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: false);
}
},
child: Text(
toDate != null ? DateFormat('dd MMM yyyy').format(toDate!) : _lang.to,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
],
),
),
SizedBox(width: 2),
RotatedBox(
quarterTurns: 1,
child: Container(
height: 1,
width: 20,
color: kSubPeraColor,
),
),
SizedBox(width: 2),
Expanded(
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
iconSize: 20,
value: selectedTime,
isExpanded: true,
items: dateOptions.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(
entry.value,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium,
),
);
}).toList(),
onChanged: (value) {
if (value == null) return;
setState(() {
selectedTime = value;
_showCustomDatePickers = value == 'custom_date';
});
if (value != 'custom_date') {
_setDateRangeFromDropdown(value);
_refreshFilteredProvider();
}
},
),
),
),
],
),
),
Divider(thickness: 1, color: kBottomBorder, height: 1),
],
),
),
iconTheme: const IconThemeData(color: Colors.black),
centerTitle: true,
elevation: 0.0,
),
body: RefreshIndicator(
onRefresh: _refreshFilteredProvider,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
headingRowColor: WidgetStatePropertyAll(Color(0xffF7F7F7)),
headingTextStyle: _theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 15,
),
columns: [
DataColumn(label: Text(_lang.name)),
DataColumn(label: Text(_lang.startDate)),
DataColumn(label: Text(_lang.endDate)),
DataColumn(label: Text(_lang.status)),
],
rows: List.generate(transaction.length, (index) {
final _transaction = transaction[index];
return DataRow(cells: [
DataCell(
GestureDetector(
onTap: () {
SubscriptionInvoicePdf.generateSaleDocument(transaction, business, context);
},
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 2,
children: [
Text(
_transaction.name ?? "N/A",
style: _theme.textTheme.titleSmall?.copyWith(
fontSize: 15,
color: DAppColors.kWarning,
),
),
Text(
_transaction.startDate == null
? "N/A"
: DateFormat('dd MMM yyyy').format(_transaction.startDate!),
style: _theme.textTheme.bodyMedium?.copyWith(
color: kPeraColor,
),
)
],
),
),
),
DataCell(Text(
_transaction.startDate == null
? "N/A"
: DateFormat('dd MMM yyyy').format(_transaction.startDate!),
textAlign: TextAlign.center,
)),
DataCell(
Text(
_transaction.endDate == null
? "N/A"
: DateFormat('dd MMM yyyy').format(_transaction.endDate!),
textAlign: TextAlign.center,
),
),
DataCell(Text.rich(
TextSpan(
children: [
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: (_transaction.isPaid ? Colors.green : const Color(0xffC52127))
.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(5),
),
child: Text(
_transaction.isPaid ? _lang.paid : _lang.unPaid,
style: TextStyle(
color: _transaction.isPaid ? Colors.green : const Color(0xffC52127),
),
),
),
),
)
],
),
))
]);
})),
),
// child: ListView.builder(
// itemCount: transaction.length,
// itemBuilder: (context, index) {
// final _transaction = transaction[index];
// return Container(
// padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
// decoration: BoxDecoration(
// border: Border(bottom: Divider.createBorderSide(context)),
// ),
// child: Column(
// mainAxisSize: MainAxisSize.min,
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Row(
// children: [
// Expanded(
// child: DefaultTextStyle.merge(
// maxLines: 1,
// overflow: TextOverflow.ellipsis,
// style: _theme.textTheme.bodyLarge?.copyWith(
// fontWeight: FontWeight.w600,
// fontSize: 15,
// ),
// child: Column(
// mainAxisSize: MainAxisSize.min,
// crossAxisAlignment: CrossAxisAlignment.start,
// spacing: 2,
// children: [
// Text(
// _transaction.name ?? "N/A",
// style: TextStyle(fontWeight: FontWeight.w600),
// ),
// Text(
// _transaction.startDate == null
// ? "N/A"
// : DateFormat('dd MMM yyyy').format(_transaction.startDate!),
// style: TextStyle(color: const Color(0xff4B5563)),
// ),
// Text('Payment By: ${_transaction.paymentBy ?? "N/A"}'),
// ],
// ),
// ),
// ),
// Expanded(
// child: DefaultTextStyle.merge(
// overflow: TextOverflow.ellipsis,
// maxLines: 1,
// textAlign: TextAlign.end,
// style: _theme.textTheme.bodyLarge?.copyWith(
// fontWeight: FontWeight.w500,
// fontSize: 15,
// color: const Color(0xff4B5563),
// ),
// child: Column(
// mainAxisSize: MainAxisSize.min,
// crossAxisAlignment: CrossAxisAlignment.end,
// spacing: 2,
// children: [
// Text(
// '${_lang.started}: ${_transaction.startDate == null ? "N/A" : DateFormat('dd MMM yyyy').format(_transaction.startDate!)}',
// ),
// Text(
// '${_lang.end}: ${_transaction.endDate == null ? "N/A" : DateFormat('dd MMM yyyy').format(_transaction.endDate!)}',
// ),
// Text.rich(
// TextSpan(
// text: '${_lang.status}: ',
// children: [
// WidgetSpan(
// alignment: PlaceholderAlignment.middle,
// child: Container(
// padding: const EdgeInsets.symmetric(horizontal: 8),
// decoration: BoxDecoration(
// color: (_transaction.isPaid
// ? Colors.green
// : const Color(0xffC52127))
// .withValues(alpha: 0.15),
// borderRadius: BorderRadius.circular(5),
// ),
// child: Text(
// _transaction.isPaid ? _lang.paid : _lang.unPaid,
// style: TextStyle(
// color: _transaction.isPaid
// ? Colors.green
// : const Color(0xffC52127),
// ),
// ),
// ),
// )
// ],
// ),
// )
// ],
// ),
// ),
// ),
// ],
// )
// ],
// ),
// );
// },
// ),
),
),
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: () => Center(child: CircularProgressIndicator()),
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: () => Center(child: CircularProgressIndicator()),
);
},
);
}
}

View File

@@ -0,0 +1,618 @@
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:iconly/iconly.dart';
import 'package:intl/intl.dart';
import 'package:mobile_pos/generated/l10n.dart' as l;
import '../../../GlobalComponents/glonal_popup.dart';
import '../../../Provider/profile_provider.dart';
import '../../../Provider/transactions_provider.dart';
import '../../../constant.dart';
import '../../../core/theme/_app_colors.dart';
import '../../../currency.dart';
import '../../../pdf_report/transactions/tax_report_pdf.dart';
import '../../../service/check_user_role_permission_provider.dart';
class TaxReportScreen extends ConsumerStatefulWidget {
const TaxReportScreen({super.key});
@override
ConsumerState<TaxReportScreen> createState() => _TaxReportScreenState();
}
class _TaxReportScreenState extends ConsumerState<TaxReportScreen> {
final TextEditingController fromDateController = TextEditingController();
final TextEditingController toDateController = TextEditingController();
final tabIndexNotifier = ValueNotifier<int>(0);
final Map<String, String> dateOptions = {
'today': l.S.current.today,
'yesterday': l.S.current.yesterday,
'last_seven_days': l.S.current.last7Days,
'last_thirty_days': l.S.current.last30Days,
'current_month': l.S.current.currentMonth,
'last_month': l.S.current.lastMonth,
'current_year': l.S.current.currentYear,
'custom_date': l.S.current.customerDate,
};
String selectedTime = 'today';
bool _isRefreshing = false;
bool _showCustomDatePickers = false;
DateTime? fromDate;
DateTime? toDate;
String searchCustomer = '';
/// Generates the date range string for the provider
FilterModel _getDateRangeFilter() {
if (_showCustomDatePickers && fromDate != null && toDate != null) {
return FilterModel(
duration: 'custom_date',
fromDate: DateFormat('yyyy-MM-dd', 'en_US').format(fromDate!),
toDate: DateFormat('yyyy-MM-dd', 'en_US').format(toDate!),
);
} else {
return FilterModel(duration: selectedTime.toLowerCase());
}
}
Future<void> _selectDate({
required BuildContext context,
required bool isFrom,
}) async {
final DateTime? picked = await showDatePicker(
context: context,
firstDate: DateTime(2021),
lastDate: DateTime.now(),
initialDate: isFrom ? fromDate ?? DateTime.now() : toDate ?? DateTime.now(),
);
if (picked != null) {
setState(() {
if (isFrom) {
fromDate = picked;
fromDateController.text = DateFormat('yyyy-MM-dd').format(picked);
} else {
toDate = picked;
toDateController.text = DateFormat('yyyy-MM-dd').format(picked);
}
});
if (fromDate != null && toDate != null) _refreshFilteredProvider();
}
}
Future<void> _refreshFilteredProvider() async {
if (_isRefreshing) return;
_isRefreshing = true;
try {
final filter = _getDateRangeFilter();
ref.refresh(filteredTaxReportReportProvider(filter));
await Future.delayed(const Duration(milliseconds: 300)); // small delay
} finally {
_isRefreshing = false;
}
}
@override
void dispose() {
fromDateController.dispose();
toDateController.dispose();
super.dispose();
}
void _updateDateUI(DateTime? from, DateTime? to) {
setState(() {
fromDate = from;
toDate = to;
fromDateController.text = from != null ? DateFormat('yyyy-MM-dd').format(from) : '';
toDateController.text = to != null ? DateFormat('yyyy-MM-dd').format(to) : '';
});
}
void _setDateRangeFromDropdown(String value) {
final now = DateTime.now();
switch (value) {
case 'today':
_updateDateUI(now, now);
break;
case 'yesterday':
final y = now.subtract(const Duration(days: 1));
_updateDateUI(y, y);
break;
case 'last_seven_days':
_updateDateUI(
now.subtract(const Duration(days: 6)),
now,
);
break;
case 'last_thirty_days':
_updateDateUI(
now.subtract(const Duration(days: 29)),
now,
);
break;
case 'current_month':
_updateDateUI(
DateTime(now.year, now.month, 1),
now,
);
break;
case 'last_month':
final first = DateTime(now.year, now.month - 1, 1);
final last = DateTime(now.year, now.month, 0);
_updateDateUI(first, last);
break;
case 'current_year':
_updateDateUI(
DateTime(now.year, 1, 1),
now,
);
break;
case 'custom_date':
// Custom: User will select manually
_updateDateUI(null, null);
break;
}
}
@override
void initState() {
super.initState();
final now = DateTime.now();
// Set initial From and To date = TODAY
fromDate = now;
toDate = now;
fromDateController.text = DateFormat('yyyy-MM-dd').format(now);
toDateController.text = DateFormat('yyyy-MM-dd').format(now);
}
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
final _lang = l.S.of(context);
return Consumer(
builder: (_, ref, watch) {
final providerData = ref.watch(filteredTaxReportReportProvider(_getDateRangeFilter()));
final personalData = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
return personalData.when(
data: (business) {
return DefaultTabController(
length: 2,
child: Builder(
builder: (tabContext) {
final tabController = DefaultTabController.of(tabContext);
tabController.addListener(() {
tabIndexNotifier.value = tabController.index;
});
return providerData.when(
data: (tx) {
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
backgroundColor: Colors.white,
title: Text(_lang.taxReportList),
actions: [
IconButton(
onPressed: () {
if ((tx.sales?.isNotEmpty == true) || (tx.purchases?.isNotEmpty == true)) {
generateTaxReportPdf(
context,
tx,
business,
fromDate,
toDate,
isPurchase: tabIndexNotifier.value == 1,
);
} else {
EasyLoading.showError(_lang.listIsEmpty);
}
},
icon: HugeIcon(icon: HugeIcons.strokeRoundedPdf02, color: kSecondayColor),
),
/*
IconButton(
onPressed: () {
if (!permissionService.hasPermission(Permit.lossProfitsRead.value)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text('You do not have permission of loss profit.'),
),
);
return;
}
if ((tx.sales?.isNotEmpty == true) || (tx.purchases?.isNotEmpty == true)) {
// generateLossProfitReportExcel(context, transaction, business, fromDate, toDate);
} else {
EasyLoading.showError('List is empty');
}
},
icon: SvgPicture.asset('assets/excel.svg'),
),
*/
SizedBox(width: 8),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(50),
child: Column(
children: [
Divider(thickness: 1, color: kBottomBorder, height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
flex: 2,
child: Row(
children: [
Icon(IconlyLight.calendar, color: kPeraColor, size: 20),
SizedBox(width: 3),
GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: true);
}
},
child: Text(
fromDate != null
? DateFormat('dd MMM yyyy').format(fromDate!)
: _lang.from,
style: Theme.of(context).textTheme.bodyMedium,
),
),
SizedBox(width: 4),
Text(
_lang.to,
style: _theme.textTheme.titleSmall,
),
SizedBox(width: 4),
Flexible(
child: GestureDetector(
onTap: () {
if (_showCustomDatePickers) {
_selectDate(context: context, isFrom: false);
}
},
child: Text(
toDate != null ? DateFormat('dd MMM yyyy').format(toDate!) : 'To',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
],
),
),
SizedBox(width: 2),
RotatedBox(
quarterTurns: 1,
child: Container(
height: 1,
width: 20,
color: kSubPeraColor,
),
),
SizedBox(width: 2),
Expanded(
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
iconSize: 20,
value: selectedTime,
isExpanded: true,
items: dateOptions.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(
entry.value,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium,
),
);
}).toList(),
onChanged: (value) {
if (value == null) return;
setState(() {
selectedTime = value;
_showCustomDatePickers = value == 'custom_date';
});
if (value != 'custom_date') {
_setDateRangeFromDropdown(value);
_refreshFilteredProvider();
}
},
),
),
),
],
),
),
Divider(thickness: 1, color: kBottomBorder, height: 1),
],
),
),
iconTheme: const IconThemeData(color: Colors.black),
centerTitle: true,
elevation: 0.0,
),
body: RefreshIndicator(
onRefresh: _refreshFilteredProvider,
child: Column(
children: [
// Overview Containers
ValueListenableBuilder(
valueListenable: tabIndexNotifier,
builder: (_, value, __) {
final _overview = [...?tx.overviews][value];
return SizedBox.fromSize(
size: Size.fromHeight(100),
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
scrollDirection: Axis.horizontal,
child: Row(
children: [
Container(
constraints: const BoxConstraints(minWidth: 170, maxHeight: 80),
decoration: BoxDecoration(
color: kSuccessColor.withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(_overview.totalAmount, addComma: true)}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
_lang.totalAmount,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
SizedBox(width: 12),
Container(
constraints: const BoxConstraints(minWidth: 170, maxHeight: 80),
decoration: BoxDecoration(
color: DAppColors.kError.withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(_overview.totalDiscount, addComma: true)}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
_lang.discount,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
SizedBox(width: 12),
Container(
constraints: const BoxConstraints(minWidth: 170, maxHeight: 80),
decoration: BoxDecoration(
color: const Color(0xffFAE3FF),
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"$currency${formatPointNumber(_overview.totalVat, addComma: true)}",
style: _theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
_lang.vat,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: kPeraColor,
),
),
],
),
),
],
),
),
);
}),
// Data
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox.fromSize(
size: const Size.fromHeight(40),
child: TabBar(
indicatorSize: TabBarIndicatorSize.tab,
unselectedLabelColor: const Color(0xff4B5563),
tabs: [
Tab(text: _lang.sales),
Tab(text: _lang.purchase),
],
),
),
Expanded(
child: ValueListenableBuilder(
valueListenable: tabIndexNotifier,
builder: (_, value, __) {
final _filteredTransactions = [
...?(value == 0 ? tx.sales : tx.purchases),
];
return ListView.builder(
itemCount: _filteredTransactions.length,
itemBuilder: (context, index) {
final _transaction = _filteredTransactions[index];
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
border: Border(bottom: Divider.createBorderSide(context)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: DefaultTextStyle.merge(
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 15,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 2,
children: [
Text(
_transaction.partyName ?? "N/A",
style: TextStyle(fontWeight: FontWeight.w600),
),
Text(_transaction.invoiceNumber ?? "N/A"),
Text(
_transaction.transactionDate == null
? "N/A"
: DateFormat('dd MMM yyyy')
.format(_transaction.transactionDate!),
style: TextStyle(color: const Color(0xff4B5563)),
),
],
),
),
),
Expanded(
child: DefaultTextStyle.merge(
overflow: TextOverflow.ellipsis,
maxLines: 1,
textAlign: TextAlign.end,
style: _theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 15,
color: const Color(0xff4B5563),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
spacing: 2,
children: [
Text(
'${_lang.amount}: $currency${formatPointNumber(_transaction.amount ?? 0, addComma: true)}',
),
Text(
'${_lang.discount}: $currency${formatPointNumber(_transaction.discountAmount ?? 0, addComma: true)}',
),
Text(
'${_transaction.vatName ?? _lang.vat}: $currency${formatPointNumber(_transaction.vatAmount ?? 0, addComma: true)}',
),
],
),
),
),
],
)
],
),
);
},
);
},
),
)
],
),
)
],
),
),
),
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: () => Center(child: CircularProgressIndicator()),
);
},
),
);
},
error: (e, stack) => Center(child: Text(e.toString())),
loading: () => Center(child: CircularProgressIndicator()),
);
},
);
}
}
extension TitleCaseExtension on String {
String toTitleCase() {
if (isEmpty) return this;
final normalized = replaceAll(RegExp(r'[_\-]+'), ' ');
final words = normalized.split(' ').map((w) => w.trim()).where((w) => w.isNotEmpty).toList();
if (words.isEmpty) return '';
final titleCased = words.map((word) {
final lower = word.toLowerCase();
return lower[0].toUpperCase() + lower.substring(1);
}).join(' ');
return titleCased;
}
}