usability fixes

This commit is contained in:
2026-06-24 18:01:34 +02:00
parent 9e55ec1097
commit a6b4a04bd5
26 changed files with 1099 additions and 216 deletions

View File

@@ -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 {
},
];
}
}
}

View File

@@ -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>
),
);
}
}
}