565 lines
22 KiB
Dart
565 lines
22 KiB
Dart
import 'dart:developer';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:auto_route/auto_route.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
|
|
|
import '../../../application/report/inventory_report/inventory_report_bloc.dart';
|
|
import '../../../application/report/transaction_report/transaction_report_bloc.dart';
|
|
import '../../../common/extension/extension.dart';
|
|
import '../../../common/theme/theme.dart';
|
|
import '../../../common/utils/pdf_service.dart';
|
|
import '../../../common/utils/permission.dart';
|
|
import '../../../injection.dart';
|
|
import '../../components/appbar/appbar.dart';
|
|
import '../../components/field/date_range_picker_field.dart';
|
|
import '../../components/report/inventory_report.dart';
|
|
import '../../components/report/transaction_report.dart';
|
|
import '../../components/toast/flushbar.dart';
|
|
|
|
@RoutePage()
|
|
class DownloadReportPage extends StatefulWidget implements AutoRouteWrapper {
|
|
const DownloadReportPage({super.key});
|
|
|
|
@override
|
|
State<DownloadReportPage> createState() => _DownloadReportPageState();
|
|
|
|
@override
|
|
Widget wrappedRoute(BuildContext context) => MultiBlocProvider(
|
|
providers: [
|
|
BlocProvider(
|
|
create: (context) =>
|
|
getIt<InventoryReportBloc>()
|
|
..add(InventoryReportEvent.fetchedOutlet()),
|
|
),
|
|
BlocProvider(
|
|
create: (context) =>
|
|
getIt<TransactionReportBloc>()
|
|
..add(TransactionReportEvent.fetchedOutlet()),
|
|
),
|
|
],
|
|
child: this,
|
|
);
|
|
}
|
|
|
|
class _DownloadReportPageState extends State<DownloadReportPage>
|
|
with TickerProviderStateMixin {
|
|
late AnimationController _fadeController;
|
|
late AnimationController _slideController;
|
|
late AnimationController _scaleController;
|
|
|
|
late Animation<double> _fadeAnimation;
|
|
|
|
// Date range variables for each report type
|
|
DateTime? _transactionStartDate;
|
|
DateTime? _transactionEndDate;
|
|
DateTime? _inventoryStartDate;
|
|
DateTime? _inventoryEndDate;
|
|
// DateTime? _salesStartDate;
|
|
// DateTime? _salesEndDate;
|
|
// DateTime? _customerStartDate;
|
|
// DateTime? _customerEndDate;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
// Initialize animation controllers
|
|
_fadeController = AnimationController(
|
|
duration: const Duration(milliseconds: 800),
|
|
vsync: this,
|
|
);
|
|
|
|
_slideController = AnimationController(
|
|
duration: const Duration(milliseconds: 1000),
|
|
vsync: this,
|
|
);
|
|
|
|
_scaleController = AnimationController(
|
|
duration: const Duration(milliseconds: 600),
|
|
vsync: this,
|
|
);
|
|
|
|
// Initialize animations
|
|
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
|
CurvedAnimation(parent: _fadeController, curve: Curves.easeInOut),
|
|
);
|
|
|
|
// Start animations
|
|
_fadeController.forward();
|
|
_slideController.forward();
|
|
_scaleController.forward();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_fadeController.dispose();
|
|
_slideController.dispose();
|
|
_scaleController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: AppColor.background,
|
|
body: CustomScrollView(
|
|
slivers: [
|
|
// SliverAppBar with gradient
|
|
SliverAppBar(
|
|
expandedHeight: 120,
|
|
floating: false,
|
|
pinned: true,
|
|
elevation: 0,
|
|
backgroundColor: AppColor.primary,
|
|
flexibleSpace: CustomAppBar(title: context.lang.download_report),
|
|
),
|
|
|
|
// Content
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
children: [
|
|
// Report Options
|
|
FadeTransition(
|
|
opacity: _fadeAnimation,
|
|
child: Column(
|
|
children: [
|
|
// Transaction Report Card
|
|
BlocBuilder<
|
|
TransactionReportBloc,
|
|
TransactionReportState
|
|
>(
|
|
builder: (context, state) {
|
|
return _ReportOptionCard(
|
|
title: context.lang.transaction_report,
|
|
subtitle: context.lang.transaction_report_desc,
|
|
icon: Icons.receipt_long_outlined,
|
|
gradient: const [
|
|
AppColor.primary,
|
|
AppColor.primaryLight,
|
|
],
|
|
startDate: _transactionStartDate,
|
|
endDate: _transactionEndDate,
|
|
isLoading: state.isFetching,
|
|
onDateRangeChanged: (start, end) {
|
|
setState(() {
|
|
_transactionStartDate = start;
|
|
_transactionEndDate = end;
|
|
});
|
|
if (start != null || end != null) {
|
|
context.read<TransactionReportBloc>().add(
|
|
TransactionReportEvent.fetchedTransaction(
|
|
start!,
|
|
end!,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
onDownload: () async {
|
|
try {
|
|
final status = await PermessionHelper()
|
|
.checkPermission();
|
|
if (status) {
|
|
final pdfFile =
|
|
await TransactionReport.previewPdf(
|
|
searchDateFormatted:
|
|
"${_transactionStartDate?.toServerDate} - ${_transactionEndDate?.toServerDate}",
|
|
outlet: state.outlet,
|
|
categoryAnalyticData:
|
|
state.categoryAnalytic,
|
|
profitLossData:
|
|
state.profitLossAnalytic,
|
|
paymentMethodAnalyticData:
|
|
state.paymentMethodAnalytic,
|
|
productAnalyticData:
|
|
state.productAnalytic,
|
|
);
|
|
log("pdfFile: $pdfFile");
|
|
await HelperPdfService.openFile(pdfFile);
|
|
} else {
|
|
AppFlushbar.showError(
|
|
context,
|
|
'Storage permission is required to save PDF',
|
|
);
|
|
}
|
|
} catch (e) {
|
|
log("Error generating PDF: $e");
|
|
AppFlushbar.showError(
|
|
context,
|
|
'Failed to generate PDF: $e',
|
|
);
|
|
}
|
|
},
|
|
delay: 200,
|
|
);
|
|
},
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
// Inventory Report Card
|
|
BlocBuilder<InventoryReportBloc, InventoryReportState>(
|
|
builder: (context, state) {
|
|
return _ReportOptionCard(
|
|
title: context.lang.invetory_report,
|
|
subtitle: context.lang.invetory_report_desc,
|
|
icon: Icons.inventory_2_outlined,
|
|
gradient: const [
|
|
AppColor.secondary,
|
|
AppColor.secondaryLight,
|
|
],
|
|
startDate: _inventoryStartDate,
|
|
endDate: _inventoryEndDate,
|
|
isLoading: state.isFetching,
|
|
onDateRangeChanged: (start, end) {
|
|
setState(() {
|
|
_inventoryStartDate = start;
|
|
_inventoryEndDate = end;
|
|
});
|
|
if (start != null || end != null) {
|
|
context.read<InventoryReportBloc>().add(
|
|
InventoryReportEvent.fetchedInventory(
|
|
start!,
|
|
end!,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
onDownload: () async {
|
|
try {
|
|
final status = await PermessionHelper()
|
|
.checkPermission();
|
|
if (status) {
|
|
final pdfFile =
|
|
await InventoryReport.previewPdf(
|
|
searchDateFormatted:
|
|
"${_inventoryStartDate?.toServerDate} - ${_inventoryEndDate?.toServerDate}",
|
|
inventory: state.inventoryAnalytic,
|
|
outlet: state.outlet,
|
|
);
|
|
log("pdfFile: $pdfFile");
|
|
await HelperPdfService.openFile(pdfFile);
|
|
} else {
|
|
AppFlushbar.showError(
|
|
context,
|
|
'Storage permission is required to save PDF',
|
|
);
|
|
}
|
|
} catch (e) {
|
|
log("Error generating PDF: $e");
|
|
AppFlushbar.showError(
|
|
context,
|
|
'Failed to generate PDF: $e',
|
|
);
|
|
}
|
|
},
|
|
delay: 400,
|
|
);
|
|
},
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
// Sales Report Card
|
|
// _ReportOptionCard(
|
|
// title: 'Sales Report',
|
|
// subtitle: 'Export sales performance and revenue data',
|
|
// icon: Icons.trending_up_outlined,
|
|
// gradient: const [AppColor.info, Color(0xFF64B5F6)],
|
|
// startDate: _salesStartDate,
|
|
// endDate: _salesEndDate,
|
|
// onDateRangeChanged: (start, end) {
|
|
// setState(() {
|
|
// _salesStartDate = start;
|
|
// _salesEndDate = end;
|
|
// });
|
|
// },
|
|
// onDownload: () => _downloadReport(
|
|
// 'Sales Report',
|
|
// _salesStartDate,
|
|
// _salesEndDate,
|
|
// ),
|
|
// delay: 600,
|
|
// ),
|
|
|
|
// const SizedBox(height: 20),
|
|
|
|
// // Customer Report Card
|
|
// _ReportOptionCard(
|
|
// title: 'Customer Report',
|
|
// subtitle:
|
|
// 'Export customer data and behavior analytics',
|
|
// icon: Icons.people_outline,
|
|
// gradient: const [AppColor.warning, Color(0xFFFFB74D)],
|
|
// startDate: _customerStartDate,
|
|
// endDate: _customerEndDate,
|
|
// onDateRangeChanged: (start, end) {
|
|
// setState(() {
|
|
// _customerStartDate = start;
|
|
// _customerEndDate = end;
|
|
// });
|
|
// },
|
|
// onDownload: () => _downloadReport(
|
|
// 'Customer Report',
|
|
// _customerStartDate,
|
|
// _customerEndDate,
|
|
// ),
|
|
// delay: 800,
|
|
// ),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 40),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ReportOptionCard extends StatefulWidget {
|
|
final String title;
|
|
final String subtitle;
|
|
final IconData icon;
|
|
final List<Color> gradient;
|
|
final DateTime? startDate;
|
|
final DateTime? endDate;
|
|
final Function(DateTime? startDate, DateTime? endDate) onDateRangeChanged;
|
|
final VoidCallback onDownload;
|
|
final bool isLoading;
|
|
final int delay;
|
|
|
|
const _ReportOptionCard({
|
|
required this.title,
|
|
required this.subtitle,
|
|
required this.icon,
|
|
required this.gradient,
|
|
required this.startDate,
|
|
required this.endDate,
|
|
required this.onDateRangeChanged,
|
|
required this.onDownload,
|
|
required this.delay,
|
|
required this.isLoading,
|
|
});
|
|
|
|
@override
|
|
State<_ReportOptionCard> createState() => _ReportOptionCardState();
|
|
}
|
|
|
|
class _ReportOptionCardState extends State<_ReportOptionCard>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _animationController;
|
|
late Animation<double> _slideAnimation;
|
|
bool _isExpanded = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_animationController = AnimationController(
|
|
duration: const Duration(milliseconds: 300),
|
|
vsync: this,
|
|
);
|
|
_slideAnimation = CurvedAnimation(
|
|
parent: _animationController,
|
|
curve: Curves.easeInOutCubic,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_animationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _toggleExpanded() {
|
|
setState(() {
|
|
_isExpanded = !_isExpanded;
|
|
if (_isExpanded) {
|
|
_animationController.forward();
|
|
} else {
|
|
_animationController.reverse();
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: widget.gradient,
|
|
),
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: widget.gradient.first.withOpacity(0.3),
|
|
blurRadius: 15,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// Header Section
|
|
GestureDetector(
|
|
onTap: _toggleExpanded,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.white.withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Icon(widget.icon, color: AppColor.white, size: 32),
|
|
),
|
|
const SizedBox(width: 20),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
widget.title,
|
|
style: AppStyle.lg.copyWith(
|
|
color: AppColor.white,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
widget.subtitle,
|
|
style: AppStyle.sm.copyWith(
|
|
color: AppColor.white.withOpacity(0.8),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
AnimatedRotation(
|
|
turns: _isExpanded ? 0.25 : 0,
|
|
duration: const Duration(milliseconds: 300),
|
|
child: Icon(
|
|
Icons.arrow_forward_ios,
|
|
color: AppColor.white.withOpacity(0.8),
|
|
size: 20,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// Expandable Content
|
|
SizeTransition(
|
|
sizeFactor: _slideAnimation,
|
|
child: Container(
|
|
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.white.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
context.lang.select_date_range,
|
|
style: AppStyle.md.copyWith(
|
|
color: AppColor.white,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Date Range Picker Field Style
|
|
DateRangePickerField(
|
|
placeholder: context.lang.select_date_range,
|
|
startDate: widget.startDate,
|
|
endDate: widget.endDate,
|
|
onChanged: widget.onDateRangeChanged,
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
// Download Button
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed:
|
|
widget.startDate != null &&
|
|
widget.endDate != null
|
|
? widget.onDownload
|
|
: null,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColor.white,
|
|
foregroundColor: widget.gradient.first,
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
elevation: 0,
|
|
),
|
|
child: widget.isLoading
|
|
? Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
SpinKitCircle(
|
|
color: widget.gradient.first,
|
|
size: 24,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Loading',
|
|
style: AppStyle.md.copyWith(
|
|
color: widget.gradient.first,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
)
|
|
: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.download_rounded,
|
|
color: widget.gradient.first,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
context.lang.download_report,
|
|
style: AppStyle.md.copyWith(
|
|
color: widget.gradient.first,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|