done all
This commit is contained in:
parent
9368cc9cc6
commit
a436c85ba6
@ -24,7 +24,7 @@ class ApiReceiptRepository implements ReceiptRepository {
|
|||||||
required String copyFor,
|
required String copyFor,
|
||||||
}) async {
|
}) async {
|
||||||
final uri = _normalizeLocalhostForAndroid(
|
final uri = _normalizeLocalhostForAndroid(
|
||||||
Uri.parse('$baseUrl/transaction/pdf-html').replace(
|
Uri.parse('$baseUrl/transaction/pdf').replace(
|
||||||
queryParameters: <String, String>{
|
queryParameters: <String, String>{
|
||||||
'transactionId': transactionId,
|
'transactionId': transactionId,
|
||||||
'copyFor': copyFor,
|
'copyFor': copyFor,
|
||||||
@ -70,7 +70,7 @@ class ApiReceiptRepository implements ReceiptRepository {
|
|||||||
required String copyFor,
|
required String copyFor,
|
||||||
}) async {
|
}) async {
|
||||||
final uri = _normalizeLocalhostForAndroid(
|
final uri = _normalizeLocalhostForAndroid(
|
||||||
Uri.parse('$baseUrl/transaction/pdf-html').replace(
|
Uri.parse('$baseUrl/transaction/pdf').replace(
|
||||||
queryParameters: <String, String>{
|
queryParameters: <String, String>{
|
||||||
'transactionId': transactionId,
|
'transactionId': transactionId,
|
||||||
'copyFor': copyFor,
|
'copyFor': copyFor,
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:e_receipt_mobile/core/config/app_config.dart';
|
import 'package:e_receipt_mobile/core/config/app_config.dart';
|
||||||
import 'package:e_receipt_mobile/data/repositories/api_receipt_repository.dart';
|
import 'package:e_receipt_mobile/data/repositories/api_receipt_repository.dart';
|
||||||
import 'package:e_receipt_mobile/domain/entities/receipt_content.dart';
|
|
||||||
import 'package:e_receipt_mobile/domain/repositories/receipt_repository.dart';
|
import 'package:e_receipt_mobile/domain/repositories/receipt_repository.dart';
|
||||||
import 'package:e_receipt_mobile/presentation/auth/session_controller.dart';
|
import 'package:e_receipt_mobile/presentation/auth/session_controller.dart';
|
||||||
import 'package:e_receipt_mobile/presentation/login/login_view_model.dart';
|
import 'package:e_receipt_mobile/presentation/login/login_view_model.dart';
|
||||||
@ -39,12 +38,6 @@ class ReceiptPdfViewData extends ReceiptViewData {
|
|||||||
final File file;
|
final File file;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ReceiptHtmlViewData extends ReceiptViewData {
|
|
||||||
const ReceiptHtmlViewData(this.html);
|
|
||||||
|
|
||||||
final String html;
|
|
||||||
}
|
|
||||||
|
|
||||||
final receiptRepositoryProvider = Provider<ReceiptRepository>((ref) {
|
final receiptRepositoryProvider = Provider<ReceiptRepository>((ref) {
|
||||||
return ApiReceiptRepository(
|
return ApiReceiptRepository(
|
||||||
baseUrl: AppConfig.apiBaseUrl,
|
baseUrl: AppConfig.apiBaseUrl,
|
||||||
@ -64,27 +57,20 @@ final transactionReceiptViewDataProvider =
|
|||||||
throw Exception('No active session');
|
throw Exception('No active session');
|
||||||
}
|
}
|
||||||
|
|
||||||
final content = await ref
|
final bytes = await ref
|
||||||
.watch(receiptRepositoryProvider)
|
.watch(receiptRepositoryProvider)
|
||||||
.getTransactionReceipt(
|
.getTransactionReceiptPdfBytes(
|
||||||
token: sessionUser.token,
|
token: sessionUser.token,
|
||||||
transactionId: query.transactionId,
|
transactionId: query.transactionId,
|
||||||
copyFor: query.copyFor,
|
copyFor: query.copyFor,
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (content) {
|
final dir = await Directory.systemTemp.createTemp('e_receipt_mobile');
|
||||||
case ReceiptPdfContent():
|
final file = File(
|
||||||
final dir = await Directory.systemTemp.createTemp(
|
'${dir.path}/receipt_${query.transactionId}_${query.copyFor}.pdf',
|
||||||
'e_receipt_mobile',
|
);
|
||||||
);
|
await file.writeAsBytes(bytes, flush: true);
|
||||||
final file = File(
|
return ReceiptPdfViewData(file);
|
||||||
'${dir.path}/receipt_${query.transactionId}_${query.copyFor}.pdf',
|
|
||||||
);
|
|
||||||
await file.writeAsBytes(content.bytes, flush: true);
|
|
||||||
return ReceiptPdfViewData(file);
|
|
||||||
case ReceiptHtmlContent():
|
|
||||||
return ReceiptHtmlViewData(content.html);
|
|
||||||
}
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint('[RECEIPT][ERROR] ${Error.safeToString(e)}\n$st');
|
debugPrint('[RECEIPT][ERROR] ${Error.safeToString(e)}\n$st');
|
||||||
throw Exception(Error.safeToString(e));
|
throw Exception(Error.safeToString(e));
|
||||||
|
|||||||
@ -439,7 +439,7 @@ class _TerminalNextScreenState extends ConsumerState<TerminalNextScreen> {
|
|||||||
_first(raw, const ['DE4']) ?? item.amount?.toString() ?? '-';
|
_first(raw, const ['DE4']) ?? item.amount?.toString() ?? '-';
|
||||||
final currency = _first(raw, const ['DE49']) ?? 'MMK';
|
final currency = _first(raw, const ['DE49']) ?? 'MMK';
|
||||||
final status = _statusLabel(
|
final status = _statusLabel(
|
||||||
_first(raw, const ['description', 'DE39', 'status']) ??
|
_first(raw, const ['DE39']) ??
|
||||||
item.status,
|
item.status,
|
||||||
);
|
);
|
||||||
final type = _first(raw, const ['DE3']) ?? item.type ?? '-';
|
final type = _first(raw, const ['DE3']) ?? item.type ?? '-';
|
||||||
@ -605,16 +605,15 @@ class _TerminalNextScreenState extends ConsumerState<TerminalNextScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _statusLabel(String? status) {
|
String _statusLabel(String? status) {
|
||||||
final normalized = (status ?? '').trim().toUpperCase();
|
final normalized = status?.trim().toUpperCase();
|
||||||
if (normalized == 'A' ||
|
if (normalized == 'A') {
|
||||||
normalized == 'SUCCESS' ||
|
|
||||||
normalized == 'PAY_SUCCESS') {
|
|
||||||
return 'SUCCESS';
|
return 'SUCCESS';
|
||||||
}
|
}
|
||||||
if (normalized == 'E' || normalized == 'FAILED') {
|
if (normalized == 'E' ) {
|
||||||
return 'FAILED';
|
return 'FAILED';
|
||||||
}
|
}
|
||||||
return normalized.isEmpty ? '-' : normalized;
|
return 'SUCCESS';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _sanitizeMultiline(String? value) {
|
String? _sanitizeMultiline(String? value) {
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:e_receipt_mobile/domain/entities/receipt_content.dart';
|
|
||||||
import 'package:e_receipt_mobile/domain/entities/terminal.dart';
|
import 'package:e_receipt_mobile/domain/entities/terminal.dart';
|
||||||
import 'package:e_receipt_mobile/domain/entities/transaction_record.dart';
|
import 'package:e_receipt_mobile/domain/entities/transaction_record.dart';
|
||||||
import 'package:e_receipt_mobile/presentation/auth/session_controller.dart';
|
import 'package:e_receipt_mobile/presentation/auth/session_controller.dart';
|
||||||
@ -13,7 +12,6 @@ import 'package:flutter_pdfview/flutter_pdfview.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:printing/printing.dart';
|
import 'package:printing/printing.dart';
|
||||||
import 'package:webview_flutter/webview_flutter.dart';
|
|
||||||
|
|
||||||
class TransactionReceiptScreen extends ConsumerWidget {
|
class TransactionReceiptScreen extends ConsumerWidget {
|
||||||
const TransactionReceiptScreen({
|
const TransactionReceiptScreen({
|
||||||
@ -146,10 +144,6 @@ class TransactionReceiptScreen extends ConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ReceiptHtmlViewData() => _ReceiptViewport(
|
|
||||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
|
|
||||||
child: _ReceiptHtmlView(html: viewData.html),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
SafeArea(
|
SafeArea(
|
||||||
@ -356,22 +350,13 @@ Future<Uint8List> _fetchReceiptPdfBytes({
|
|||||||
throw Exception('No active session');
|
throw Exception('No active session');
|
||||||
}
|
}
|
||||||
|
|
||||||
final content = await ref.read(receiptRepositoryProvider).getTransactionReceipt(
|
return ref
|
||||||
|
.read(receiptRepositoryProvider)
|
||||||
|
.getTransactionReceiptPdfBytes(
|
||||||
token: sessionUser.token,
|
token: sessionUser.token,
|
||||||
transactionId: transactionId,
|
transactionId: transactionId,
|
||||||
copyFor: copyFor,
|
copyFor: copyFor,
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (content) {
|
|
||||||
case ReceiptPdfContent():
|
|
||||||
return content.bytes;
|
|
||||||
case ReceiptHtmlContent():
|
|
||||||
try {
|
|
||||||
return await Printing.convertHtml(html: _wrapHtml(content.html));
|
|
||||||
} catch (e) {
|
|
||||||
throw Exception('Server did not return PDF, and HTML→PDF failed: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<T> _runWithProgress<T>(
|
Future<T> _runWithProgress<T>(
|
||||||
@ -416,146 +401,3 @@ Future<T> _runWithProgress<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ReceiptHtmlWebView extends StatefulWidget {
|
|
||||||
const _ReceiptHtmlWebView({required this.html});
|
|
||||||
|
|
||||||
final String html;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_ReceiptHtmlWebView> createState() => _ReceiptHtmlWebViewState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ReceiptHtmlWebViewState extends State<_ReceiptHtmlWebView> {
|
|
||||||
late final WebViewController _controller;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
_controller = WebViewController()
|
|
||||||
..setJavaScriptMode(JavaScriptMode.disabled)
|
|
||||||
..setBackgroundColor(Colors.transparent)
|
|
||||||
..setNavigationDelegate(
|
|
||||||
NavigationDelegate(
|
|
||||||
onNavigationRequest: (request) => NavigationDecision.prevent,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
..loadHtmlString(_wrapHtml(widget.html));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return WebViewWidget(controller: _controller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ReceiptHtmlView extends StatelessWidget {
|
|
||||||
const _ReceiptHtmlView({required this.html});
|
|
||||||
|
|
||||||
final String html;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final platform = defaultTargetPlatform;
|
|
||||||
final supportsWebView =
|
|
||||||
platform == TargetPlatform.android || platform == TargetPlatform.iOS;
|
|
||||||
if (!supportsWebView) {
|
|
||||||
return _ReceiptHtmlFallbackText(html: html);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _ReceiptHtmlWebView(html: html);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ReceiptHtmlFallbackText extends StatelessWidget {
|
|
||||||
const _ReceiptHtmlFallbackText({required this.html});
|
|
||||||
|
|
||||||
final String html;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final plainText = _htmlToPlainText(html);
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: SelectionArea(
|
|
||||||
child: DefaultTextStyle(
|
|
||||||
style:
|
|
||||||
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
height: 1.35,
|
|
||||||
) ??
|
|
||||||
const TextStyle(fontFamily: 'monospace', height: 1.35),
|
|
||||||
child: Text(plainText),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _wrapHtml(String html) {
|
|
||||||
// Keep backend HTML intact but ensure a sane viewport and remove default
|
|
||||||
// margins so it looks like the web "receipt iframe" container.
|
|
||||||
return '''
|
|
||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<style>
|
|
||||||
html, body { margin: 0; padding: 0; background: transparent; }
|
|
||||||
body { -webkit-text-size-adjust: 100%; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>$html</body>
|
|
||||||
</html>
|
|
||||||
''';
|
|
||||||
}
|
|
||||||
|
|
||||||
String _htmlToPlainText(String html) {
|
|
||||||
var s = html;
|
|
||||||
|
|
||||||
s = s.replaceAll(
|
|
||||||
RegExp(
|
|
||||||
r'<(script|style)[^>]*>.*?</\1>',
|
|
||||||
caseSensitive: false,
|
|
||||||
dotAll: true,
|
|
||||||
),
|
|
||||||
'',
|
|
||||||
);
|
|
||||||
s = s.replaceAll(RegExp(r'<br\s*/?>', caseSensitive: false), '\n');
|
|
||||||
s = s.replaceAll(
|
|
||||||
RegExp(
|
|
||||||
r'</(div|p|tr|pre|table|section|header|footer)>',
|
|
||||||
caseSensitive: false,
|
|
||||||
),
|
|
||||||
'\n',
|
|
||||||
);
|
|
||||||
s = s.replaceAll(RegExp(r'<li[^>]*>', caseSensitive: false), '• ');
|
|
||||||
s = s.replaceAll(RegExp(r'</li>', caseSensitive: false), '\n');
|
|
||||||
|
|
||||||
s = s.replaceAll(RegExp(r'<[^>]+>'), '');
|
|
||||||
|
|
||||||
s = s.replaceAll(' ', ' ');
|
|
||||||
s = s.replaceAll('&', '&');
|
|
||||||
s = s.replaceAll('<', '<');
|
|
||||||
s = s.replaceAll('>', '>');
|
|
||||||
s = s.replaceAll('"', '"');
|
|
||||||
s = s.replaceAll(''', "'");
|
|
||||||
|
|
||||||
s = s.replaceAllMapped(RegExp(r'&#(\d+);'), (m) {
|
|
||||||
final code = int.tryParse(m.group(1) ?? '');
|
|
||||||
if (code == null) return '';
|
|
||||||
return String.fromCharCode(code);
|
|
||||||
});
|
|
||||||
s = s.replaceAllMapped(RegExp(r'&#x([0-9a-fA-F]+);'), (m) {
|
|
||||||
final code = int.tryParse(m.group(1) ?? '', radix: 16);
|
|
||||||
if (code == null) return '';
|
|
||||||
return String.fromCharCode(code);
|
|
||||||
});
|
|
||||||
|
|
||||||
s = s.replaceAll(RegExp(r'[ \t]+\n'), '\n');
|
|
||||||
s = s.replaceAll(RegExp(r'\n{3,}'), '\n\n');
|
|
||||||
|
|
||||||
return s.trim();
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:e_receipt_mobile/domain/entities/terminal.dart';
|
import 'package:e_receipt_mobile/domain/entities/terminal.dart';
|
||||||
@ -57,6 +59,7 @@ class TerminalListView extends StatelessWidget {
|
|||||||
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final terminal = terminals[index];
|
final terminal = terminals[index];
|
||||||
|
print('this is terminal ${terminal.status}');
|
||||||
final enabled = terminal.tid != null || terminal.id != null;
|
final enabled = terminal.tid != null || terminal.id != null;
|
||||||
final title =
|
final title =
|
||||||
(_sanitizeMultiline(terminal.name)?.isNotEmpty ?? false)
|
(_sanitizeMultiline(terminal.name)?.isNotEmpty ?? false)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user