first commit
This commit is contained in:
532
lib/Screens/Report/Screens/balance_sheet_screen.dart
Normal file
532
lib/Screens/Report/Screens/balance_sheet_screen.dart
Normal 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()),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
731
lib/Screens/Report/Screens/bill_wise_profit_screen.dart
Normal file
731
lib/Screens/Report/Screens/bill_wise_profit_screen.dart
Normal 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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
631
lib/Screens/Report/Screens/cashflow_screen.dart
Normal file
631
lib/Screens/Report/Screens/cashflow_screen.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
614
lib/Screens/Report/Screens/day_book_report.dart
Normal file
614
lib/Screens/Report/Screens/day_book_report.dart
Normal 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()}";
|
||||
}
|
||||
}
|
||||
917
lib/Screens/Report/Screens/due_report_screen.dart
Normal file
917
lib/Screens/Report/Screens/due_report_screen.dart
Normal 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()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
587
lib/Screens/Report/Screens/expense_report.dart
Normal file
587
lib/Screens/Report/Screens/expense_report.dart
Normal 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()),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
755
lib/Screens/Report/Screens/expire_report.dart
Normal file
755
lib/Screens/Report/Screens/expire_report.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
735
lib/Screens/Report/Screens/purchase_report.dart
Normal file
735
lib/Screens/Report/Screens/purchase_report.dart
Normal 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()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
722
lib/Screens/Report/Screens/purchase_return_report.dart
Normal file
722
lib/Screens/Report/Screens/purchase_return_report.dart
Normal 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()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
513
lib/Screens/Report/Screens/sales_report_screen.dart
Normal file
513
lib/Screens/Report/Screens/sales_report_screen.dart
Normal 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());
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
547
lib/Screens/Report/Screens/sales_return_report_screen.dart
Normal file
547
lib/Screens/Report/Screens/sales_return_report_screen.dart
Normal 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());
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
552
lib/Screens/Report/Screens/subscription_report_screen.dart
Normal file
552
lib/Screens/Report/Screens/subscription_report_screen.dart
Normal 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()),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
618
lib/Screens/Report/Screens/tax_report.dart
Normal file
618
lib/Screens/Report/Screens/tax_report.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user