usability fixes
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:qpay/config/app_config.dart';
|
||||
import 'package:qpay/http/http.dart';
|
||||
import 'package:qpay/models/api_response.dart';
|
||||
import 'package:qpay/models/pageable_model.dart';
|
||||
@@ -183,6 +186,58 @@ class HistoryController extends ChangeNotifier {
|
||||
_safeNotify();
|
||||
}
|
||||
|
||||
/// Builds the fully-qualified download URL for the transactions report.
|
||||
Uri _buildReportUrl({
|
||||
required String workspaceId,
|
||||
required DateTime startDate,
|
||||
required DateTime endDate,
|
||||
}) {
|
||||
final df = DateFormat("yyyy-MM-dd'T'HH:mm:ss");
|
||||
final start = Uri.encodeComponent(
|
||||
df.format(
|
||||
DateTime(startDate.year, startDate.month, startDate.day, 0, 0, 0),
|
||||
),
|
||||
);
|
||||
final end = Uri.encodeComponent(
|
||||
df.format(
|
||||
DateTime(endDate.year, endDate.month, endDate.day, 23, 59, 59),
|
||||
),
|
||||
);
|
||||
return Uri.parse(
|
||||
'${AppConfig.baseUrl}/public/reports/transactions/download'
|
||||
'?startDate=$start'
|
||||
'&endDate=$end'
|
||||
'&workspaceId=$workspaceId',
|
||||
);
|
||||
}
|
||||
|
||||
/// Opens the transactions report download URL in the device browser.
|
||||
/// The browser handles the actual file download natively — no file I/O
|
||||
/// or plugin channels required, works on Android, iOS, and web.
|
||||
Future<void> downloadReport({
|
||||
required String workspaceId,
|
||||
required DateTime startDate,
|
||||
required DateTime endDate,
|
||||
}) async {
|
||||
model.isActing = true;
|
||||
_safeNotify();
|
||||
|
||||
try {
|
||||
final uri = _buildReportUrl(
|
||||
workspaceId: workspaceId,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
);
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} catch (e) {
|
||||
logger.e('Failed to open report URL: $e');
|
||||
model.errorMessage = 'Failed to download report';
|
||||
} finally {
|
||||
model.isActing = false;
|
||||
_safeNotify();
|
||||
}
|
||||
}
|
||||
|
||||
void clearFilters() {
|
||||
model.statusFilter = null;
|
||||
model.typeFilter = null;
|
||||
@@ -219,4 +274,4 @@ class HistoryController extends ChangeNotifier {
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qpay/auth_state.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/receipt/receipt_controller.dart';
|
||||
import 'package:qpay/screens/transaction_controller.dart';
|
||||
@@ -251,6 +253,142 @@ class _HistoryScreenState extends State<HistoryScreen>
|
||||
);
|
||||
}
|
||||
|
||||
/// Shows the report download bottom sheet with start/end date pickers.
|
||||
void _showReportSheet() {
|
||||
DateTime selectedStart = DateTime.now().subtract(const Duration(days: 7));
|
||||
DateTime selectedEnd = DateTime.now();
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (ctx) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setSheetState) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 20,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + 20,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Download Report',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildDateField(
|
||||
label: 'Start Date',
|
||||
date: selectedStart,
|
||||
onTap: () async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: selectedStart,
|
||||
firstDate: DateTime(2020),
|
||||
lastDate: selectedEnd,
|
||||
);
|
||||
if (picked != null) {
|
||||
setSheetState(() => selectedStart = picked);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildDateField(
|
||||
label: 'End Date',
|
||||
date: selectedEnd,
|
||||
onTap: () async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: selectedEnd,
|
||||
firstDate: selectedStart,
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
if (picked != null) {
|
||||
setSheetState(() => selectedEnd = picked);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
icon: controller.model.isActing
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.download),
|
||||
label: Text(
|
||||
controller.model.isActing
|
||||
? 'Downloading…'
|
||||
: 'Download Report',
|
||||
),
|
||||
onPressed: controller.model.isActing
|
||||
? null
|
||||
: () {
|
||||
Navigator.of(ctx).pop();
|
||||
final workspaceId =
|
||||
prefs.getString("workspaceId") ?? '';
|
||||
controller.downloadReport(
|
||||
workspaceId: workspaceId,
|
||||
startDate: selectedStart,
|
||||
endDate: selectedEnd,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds a tappable date field for the report sheet.
|
||||
Widget _buildDateField({
|
||||
required String label,
|
||||
required DateTime date,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
final df = DateFormat('dd MMM yyyy');
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'$label: ${df.format(date)}',
|
||||
style: const TextStyle(fontSize: 15),
|
||||
),
|
||||
),
|
||||
Icon(Icons.calendar_today, size: 20, color: Colors.grey.shade600),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
@@ -396,6 +534,15 @@ class _HistoryScreenState extends State<HistoryScreen>
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Report download — only visible to logged-in users.
|
||||
if (authState.isLoggedIn)
|
||||
_buildIconButton(
|
||||
icon: Icons.description_outlined,
|
||||
tooltip: 'Download report',
|
||||
isActive: false,
|
||||
onPressed: _showReportSheet,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildIconButton(
|
||||
icon: Icons.add,
|
||||
tooltip: 'New payment',
|
||||
@@ -679,4 +826,4 @@ class _HistoryScreenState extends State<HistoryScreen>
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user