bottleneckcrm/mobile/lib/services/record_export.dart
Ashwin d920cc294f chore: update dependencies and version in pubspec.yaml
- 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
2026-09-27 18:58:13 +05:30

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();