This commit is contained in:
kyawkhantwin 2026-04-01 14:59:18 +06:30
parent 9368cc9cc6
commit a436c85ba6
5 changed files with 22 additions and 192 deletions

View File

@ -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,

View File

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

View File

@ -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) {

View File

@ -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('&nbsp;', ' ');
s = s.replaceAll('&amp;', '&');
s = s.replaceAll('&lt;', '<');
s = s.replaceAll('&gt;', '>');
s = s.replaceAll('&quot;', '"');
s = s.replaceAll('&#39;', "'");
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();
}

View File

@ -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)