feat(frontend): implement print queue and fix document download issues
This commit is contained in:
1 parent
fe8f9be585
commit
c8780c53db
7 files changed
+231
-64
No files matched your search
@@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import '../../core/network/api_constants.dart';
|
||||
@@ -600,5 +602,33 @@ class DocumentRepository {
|
||||
throw Exception(msg);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<int>> downloadDocumentBytes(int documentId) async {
|
||||
try {
|
||||
final options = await _getAuthOptions();
|
||||
options.responseType = ResponseType.bytes;
|
||||
final response = await _dio.get(
|
||||
'/documents/$documentId/view',
|
||||
queryParameters: {'download': 'true'},
|
||||
options: options,
|
||||
);
|
||||
return response.data as List<int>;
|
||||
} on DioException catch (e) {
|
||||
String msg = 'Failed to download document.';
|
||||
if (e.response?.data != null) {
|
||||
try {
|
||||
// If response is bytes, we might need to decode it to string to parse JSON
|
||||
if (e.response!.data is List<int>) {
|
||||
final str = utf8.decode(e.response!.data as List<int>);
|
||||
final json = jsonDecode(str);
|
||||
msg = json['message'] ?? msg;
|
||||
} else if (e.response!.data is Map) {
|
||||
msg = e.response!.data['message'] ?? msg;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
throw Exception(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,7 +291,9 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
if (token == null) return;
|
||||
final url = '${ApiConstants.baseUrl}/documents/${document.id}/view?token=$token&download=true';
|
||||
|
||||
showSuccessDialog(context, message: 'Download started...');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Download started...'), duration: Duration(seconds: 2)),
|
||||
);
|
||||
await DownloadService.downloadFile(url: url, fileName: document.fileName);
|
||||
if (!context.mounted) return;
|
||||
showSuccessDialog(context, message: 'Document downloaded successfully.');
|
||||
@@ -407,7 +409,9 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
final token = await const FlutterSecureStorage().read(key: 'jwt_token');
|
||||
if (token == null) return;
|
||||
|
||||
showSuccessDialog(context, message: 'Downloading ${_selectedDocumentIds.length} documents...');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Downloading ${_selectedDocumentIds.length} documents...'), duration: const Duration(seconds: 2)),
|
||||
);
|
||||
|
||||
final docs = ref.read(documentPageProvider).value?.documents ?? [];
|
||||
int successCount = 0;
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:printing/printing.dart';
|
||||
import 'package:syncfusion_flutter_pdf/pdf.dart';
|
||||
import 'package:wolt_modal_sheet/wolt_modal_sheet.dart';
|
||||
|
||||
import '../../../../core/constants/app_colors.dart';
|
||||
import '../../../../core/constants/app_sizes.dart';
|
||||
import '../../../../domain/repositories/document_repository.dart'
|
||||
@@ -8,7 +13,6 @@ import '../../../shared/buttons/primary_button.dart';
|
||||
import '../../../shared/dialogs/success_dialog.dart';
|
||||
import '../../../shared/dialogs/error_dialog.dart';
|
||||
import '../../../providers/document_provider.dart';
|
||||
import 'package:wolt_modal_sheet/wolt_modal_sheet.dart';
|
||||
|
||||
class PrintQueueModal extends ConsumerStatefulWidget {
|
||||
const PrintQueueModal({super.key});
|
||||
@@ -41,6 +45,74 @@ class _PrintQueueModalState extends ConsumerState<PrintQueueModal> {
|
||||
setState(() => _isPrinting = true);
|
||||
|
||||
try {
|
||||
final docRepo = ref.read(documentRepositoryProvider);
|
||||
|
||||
// Merge all documents into a single PDF
|
||||
final combinedPdf = PdfDocument();
|
||||
|
||||
for (var item in items) {
|
||||
final bytes = await docRepo.downloadDocumentBytes(item.documentId);
|
||||
final isPdf = item.fileName.toLowerCase().endsWith('.pdf');
|
||||
|
||||
if (isPdf) {
|
||||
final loadedPdf = PdfDocument(inputBytes: bytes);
|
||||
for (int i = 0; i < loadedPdf.pages.count; i++) {
|
||||
final template = loadedPdf.pages[i].createTemplate();
|
||||
final page = combinedPdf.pages.add();
|
||||
page.graphics.drawPdfTemplate(template, const Offset(0, 0));
|
||||
}
|
||||
loadedPdf.dispose();
|
||||
} else {
|
||||
// Assume image
|
||||
final page = combinedPdf.pages.add();
|
||||
final pdfImage = PdfBitmap(bytes);
|
||||
|
||||
// Calculate scale to fit page while maintaining aspect ratio
|
||||
final clientSize = page.getClientSize();
|
||||
final imgWidth = pdfImage.width.toDouble();
|
||||
final imgHeight = pdfImage.height.toDouble();
|
||||
|
||||
final ratio = imgWidth / imgHeight;
|
||||
final clientRatio = clientSize.width / clientSize.height;
|
||||
|
||||
double drawWidth = clientSize.width;
|
||||
double drawHeight = clientSize.height;
|
||||
|
||||
if (ratio > clientRatio) {
|
||||
drawHeight = drawWidth / ratio;
|
||||
} else {
|
||||
drawWidth = drawHeight * ratio;
|
||||
}
|
||||
|
||||
page.graphics.drawImage(
|
||||
pdfImage,
|
||||
Rect.fromLTWH(
|
||||
(clientSize.width - drawWidth) / 2,
|
||||
(clientSize.height - drawHeight) / 2,
|
||||
drawWidth,
|
||||
drawHeight
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final List<int> combinedBytes = await combinedPdf.save();
|
||||
combinedPdf.dispose();
|
||||
|
||||
// Send to printer
|
||||
final result = await Printing.layoutPdf(
|
||||
onLayout: (format) async => Uint8List.fromList(combinedBytes),
|
||||
name: 'Batch_Print_${DateTime.now().millisecondsSinceEpoch}',
|
||||
);
|
||||
|
||||
// If the user cancelled the print dialog, we might not want to clear the queue
|
||||
if (!result) {
|
||||
if (!mounted) return;
|
||||
setState(() => _isPrinting = false);
|
||||
return; // User cancelled print dialog
|
||||
}
|
||||
|
||||
// Log history and clear queue in backend
|
||||
await ref.read(printQueueMutationProvider.notifier).executePrint();
|
||||
|
||||
if (!mounted) return;
|
||||
@@ -113,69 +185,69 @@ class _PrintQueueModalState extends ConsumerState<PrintQueueModal> {
|
||||
children: [
|
||||
// ── Header ──
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryGreen.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(Icons.print,
|
||||
color: AppColors.primaryGreen, size: 22),
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryGreen.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
const SizedBox(width: AppSizes.p12),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Print List',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary)),
|
||||
Text('Staged documents. Note: This is only a list of documents to be printed or requested.',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: AppColors.textSecondary,
|
||||
fontStyle: FontStyle.italic)),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close,
|
||||
color: AppColors.textSecondary),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: AppSizes.p32),
|
||||
|
||||
// ── Queue Content ──
|
||||
queueAsync.when(
|
||||
loading: () => const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 40),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColors.primaryGreen),
|
||||
child: const Icon(Icons.print,
|
||||
color: AppColors.primaryGreen, size: 22),
|
||||
),
|
||||
const SizedBox(width: AppSizes.p12),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Print List',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary)),
|
||||
Text('Staged documents. Note: This is only a list of documents to be printed or requested.',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: AppColors.textSecondary,
|
||||
fontStyle: FontStyle.italic)),
|
||||
],
|
||||
),
|
||||
),
|
||||
error: (e, _) => _buildErrorState(e.toString()),
|
||||
data: (items) => items.isEmpty
|
||||
? _buildEmptyState()
|
||||
: _buildQueueList(items),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close,
|
||||
color: AppColors.textSecondary),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: AppSizes.p32),
|
||||
|
||||
// ── Footer ──
|
||||
queueAsync.maybeWhen(
|
||||
data: (items) => items.isNotEmpty
|
||||
? _buildFooter(items, isMobile)
|
||||
: const SizedBox.shrink(),
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
// ── Queue Content ──
|
||||
queueAsync.when(
|
||||
loading: () => const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 40),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColors.primaryGreen),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
error: (e, _) => _buildErrorState(e.toString()),
|
||||
data: (items) => items.isEmpty
|
||||
? _buildEmptyState()
|
||||
: _buildQueueList(items),
|
||||
),
|
||||
|
||||
// ── Footer ──
|
||||
queueAsync.maybeWhen(
|
||||
data: (items) => items.isNotEmpty
|
||||
? _buildFooter(items, isMobile)
|
||||
: const SizedBox.shrink(),
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQueueList(List<PrintQueueItem> items) {
|
||||
@@ -267,7 +339,6 @@ class _PrintQueueModalState extends ConsumerState<PrintQueueModal> {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildFooter(List<PrintQueueItem> items, bool isMobile) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@@ -297,7 +368,7 @@ class _PrintQueueModalState extends ConsumerState<PrintQueueModal> {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
label: 'PRINT',
|
||||
label: _isPrinting ? 'PREPARING...' : 'PRINT',
|
||||
isLoading: _isPrinting,
|
||||
onPressed: () => _handlePrintAll(items),
|
||||
),
|
||||
@@ -332,9 +403,9 @@ class _PrintQueueModalState extends ConsumerState<PrintQueueModal> {
|
||||
),
|
||||
const SizedBox(width: AppSizes.p8),
|
||||
SizedBox(
|
||||
width: 130,
|
||||
width: 140,
|
||||
child: PrimaryButton(
|
||||
label: 'PRINT',
|
||||
label: _isPrinting ? 'PREPARING...' : 'PRINT',
|
||||
isLoading: _isPrinting,
|
||||
onPressed: () => _handlePrintAll(items),
|
||||
),
|
||||
|
||||
@@ -57,6 +57,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
barcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: barcode
|
||||
sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.9"
|
||||
bidi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bidi
|
||||
sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.13"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -768,6 +784,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_parsing
|
||||
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -816,6 +840,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
pdf:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pdf
|
||||
sha256: e47a275b267873d5944ad5f5ff0dcc7ac2e36c02b3046a0ffac9b72fd362c44b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.12.0"
|
||||
pdf_widget_wrapper:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pdf_widget_wrapper
|
||||
sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
permission_handler:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -896,6 +936,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
printing:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: printing
|
||||
sha256: "689170c9ddb1bda85826466ba80378aa8993486d3c959a71cd7d2d80cb606692"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.14.3"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -904,6 +952,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
qr:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: qr
|
||||
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
record_use:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -57,6 +57,8 @@ dependencies:
|
||||
google_mlkit_document_scanner: ^0.4.1
|
||||
data_table_2: ^2.7.2
|
||||
wolt_modal_sheet: ^0.11.0
|
||||
printing: ^5.14.3
|
||||
pdf: ^3.12.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <printing/printing_plugin.h>
|
||||
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
||||
#include <syncfusion_pdfviewer_windows/syncfusion_pdfviewer_windows_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
@@ -21,6 +22,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||
PrintingPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
||||
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
|
||||
SyncfusionPdfviewerWindowsPluginRegisterWithRegistrar(
|
||||
|
||||
@@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_windows
|
||||
flutter_secure_storage_windows
|
||||
permission_handler_windows
|
||||
printing
|
||||
screen_retriever_windows
|
||||
syncfusion_pdfviewer_windows
|
||||
url_launcher_windows
|
||||
|
||||
Reference in new issue
Block a user