- Bump version to 1.10.0+14 - Update http from ^1.1.0 to ^1.6.0 - Update google_sign_in from ^7.1.1 to ^7.2.0 - Update flutter_secure_storage from ^10.3.1 to ^11.2.0 - Update firebase_core from ^4.12.1 to ^4.15.0 - Update firebase_crashlytics from ^5.2.6 to ^5.4.0 - Update go_router from ^17.2.3 to ^18.0.1 - Update flutter_riverpod from ^3.3.1 to ^3.4.3 - Update table_calendar from ^3.2.0 to ^3.2.1 - Update intl from ^0.20.2 to ^0.20.3 - Update url_launcher from ^6.3.1 to ^6.3.2 - Update file_picker from ^11.0.3 to ^13.1.0 - Update google_fonts from ^8.1.0 to ^8.2.1 - Update lucide_icons_flutter from ^3.1.15 to ^3.1.20 - Add cross_file dependency for testing test: add FakePlatformFile for testing file uploads - Introduce FakePlatformFile to mock file picker behavior in tests - Update attachment_upload_test, csv_import_provider_test, support_ticket_test, and csv_import_sheet_test to use FakePlatformFile
89 lines
3.2 KiB
Dart
89 lines
3.2 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import 'package:file_picker/file_picker.dart';
|
|
|
|
import 'api_service.dart';
|
|
|
|
/// How an export ended. [cancelled] is not a failure: the person closed the
|
|
/// save dialog, and nothing should be said about it.
|
|
enum RecordExportOutcome { saved, cancelled, failed }
|
|
|
|
class RecordExportResult {
|
|
const RecordExportResult(this.outcome, {this.fileName, this.error});
|
|
|
|
final RecordExportOutcome outcome;
|
|
final String? fileName;
|
|
final String? error;
|
|
}
|
|
|
|
/// `<prefix>-<YYYY-MM-DD>.csv`, the name the API gives the web download.
|
|
///
|
|
/// The day is the device's. The API names its file by the org's day, and a
|
|
/// phone in the org's timezone agrees with it; `getBytes` does not surface the
|
|
/// response headers, so the name cannot be read off the reply.
|
|
String exportFileName(String prefix, DateTime day) {
|
|
String two(int n) => n.toString().padLeft(2, '0');
|
|
return '$prefix-${day.year}-${two(day.month)}-${two(day.day)}.csv';
|
|
}
|
|
|
|
/// [endpoint] with [query] as its query string. A `List` value becomes a
|
|
/// repeated parameter (`status=New&status=Pending`), which is how the list
|
|
/// endpoints take more than one of a thing. Empty values are left out.
|
|
String exportUrl(String endpoint, Map<String, Object?> query) {
|
|
final params = <String, dynamic>{};
|
|
query.forEach((key, value) {
|
|
if (value is Iterable) {
|
|
final values = value.map((v) => '$v').where((v) => v.isNotEmpty).toList();
|
|
if (values.isNotEmpty) params[key] = values;
|
|
} else if (value != null && '$value'.isNotEmpty) {
|
|
params[key] = '$value';
|
|
}
|
|
});
|
|
final uri = Uri.parse(endpoint);
|
|
return (params.isEmpty ? uri : uri.replace(queryParameters: params))
|
|
.toString();
|
|
}
|
|
|
|
/// Download a list as CSV with the list's own filters, then ask the operating
|
|
/// system where to keep it.
|
|
///
|
|
/// The API decides which rows the caller may have, exactly as it does for the
|
|
/// list: this sends the same query and trusts nothing it holds locally.
|
|
/// [api], [saveFile] and [today] are injectable for tests.
|
|
Future<RecordExportResult> exportRecordsCsv({
|
|
required String endpoint,
|
|
required Map<String, Object?> query,
|
|
required String filePrefix,
|
|
ApiService? api,
|
|
Future<String?> Function(Uint8List bytes, String fileName)? saveFile,
|
|
DateTime? today,
|
|
}) async {
|
|
final response = await (api ?? ApiService()).getBytes(
|
|
exportUrl(endpoint, query),
|
|
);
|
|
if (!response.success || response.data == null) {
|
|
return RecordExportResult(
|
|
RecordExportOutcome.failed,
|
|
error: response.message ?? 'Could not export.',
|
|
);
|
|
}
|
|
final name = exportFileName(filePrefix, today ?? DateTime.now());
|
|
try {
|
|
final saved = await (saveFile ?? _saveWithPicker)(response.data!, name);
|
|
return saved == null
|
|
? const RecordExportResult(RecordExportOutcome.cancelled)
|
|
: RecordExportResult(RecordExportOutcome.saved, fileName: name);
|
|
} catch (_) {
|
|
return const RecordExportResult(
|
|
RecordExportOutcome.failed,
|
|
error: 'Could not save the file.',
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<String?> _saveWithPicker(Uint8List bytes, String fileName) async =>
|
|
(await FilePicker.saveFile(
|
|
dialogTitle: 'Save CSV export',
|
|
fileName: fileName,
|
|
bytes: bytes,
|
|
))?.toString();
|