619 lines
31 KiB
Dart
619 lines
31 KiB
Dart
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${formatWithSeparator(_overview.totalAmount)}",
|
|
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${formatWithSeparator(_overview.totalDiscount)}",
|
|
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${formatWithSeparator(_overview.totalVat)}",
|
|
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${formatWithSeparator(_transaction.amount ?? 0)}',
|
|
),
|
|
Text(
|
|
'${_lang.discount}: $currency${formatWithSeparator(_transaction.discountAmount ?? 0)}',
|
|
),
|
|
Text(
|
|
'${_transaction.vatName ?? _lang.vat}: $currency${formatWithSeparator(_transaction.vatAmount ?? 0)}',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
)
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
)
|
|
],
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
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;
|
|
}
|
|
}
|