A more robust pdf generation > preview > print workflow
This commit is contained in:
parent
3a923ea7f6
commit
db14ec3916
BIN
assets/fonts/Roboto-Bold.ttf
Normal file
BIN
assets/fonts/Roboto-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Roboto-Regular.ttf
Normal file
BIN
assets/fonts/Roboto-Regular.ttf
Normal file
Binary file not shown.
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
import 'package:audioplayers/audioplayers.dart';
|
import 'package:audioplayers/audioplayers.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:pdfrx/pdfrx.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
|
|
@ -36,6 +37,8 @@ Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
// The flag optionally hides annoying WASM warnings in your Chrome dev console
|
||||||
|
pdfrxFlutterInitialize(dismissPdfiumWasmWarnings: true);
|
||||||
|
|
||||||
// initialize Firebase before anything that uses messaging
|
// initialize Firebase before anything that uses messaging
|
||||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||||
|
|
@ -192,7 +195,10 @@ class NotificationSoundObserver extends ProviderObserver {
|
||||||
controller.registerFcmToken(token);
|
controller.registerFcmToken(token);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catchError((e) => debugPrint('getToken error: $e'));
|
.catchError((e) {
|
||||||
|
debugPrint('getToken error: $e');
|
||||||
|
return null;
|
||||||
|
});
|
||||||
_tokenSub = FirebaseMessaging.instance.onTokenRefresh.listen((token) {
|
_tokenSub = FirebaseMessaging.instance.onTokenRefresh.listen((token) {
|
||||||
debugPrint('token refreshed: $token');
|
debugPrint('token refreshed: $token');
|
||||||
controller.registerFcmToken(token);
|
controller.registerFcmToken(token);
|
||||||
|
|
@ -210,7 +216,10 @@ class NotificationSoundObserver extends ProviderObserver {
|
||||||
controller.unregisterFcmToken(token);
|
controller.unregisterFcmToken(token);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catchError((e) => debugPrint('getToken error: $e'));
|
.catchError((e) {
|
||||||
|
debugPrint('getToken error: $e');
|
||||||
|
return null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import 'package:flutter_quill/flutter_quill.dart' as quill;
|
||||||
import 'package:pdf/widgets.dart' as pw;
|
import 'package:pdf/widgets.dart' as pw;
|
||||||
import 'package:pdf/pdf.dart' as pdf;
|
import 'package:pdf/pdf.dart' as pdf;
|
||||||
import 'package:printing/printing.dart';
|
import 'package:printing/printing.dart';
|
||||||
|
import 'package:pdfrx/pdfrx.dart';
|
||||||
|
|
||||||
import '../../models/task.dart';
|
import '../../models/task.dart';
|
||||||
import '../../models/ticket.dart';
|
import '../../models/ticket.dart';
|
||||||
|
|
@ -27,6 +28,14 @@ Future<Uint8List> buildTaskPdfBytes(
|
||||||
) async {
|
) async {
|
||||||
final logoData = await rootBundle.load('assets/crmc_logo.png');
|
final logoData = await rootBundle.load('assets/crmc_logo.png');
|
||||||
final logoImage = pw.MemoryImage(logoData.buffer.asUint8List());
|
final logoImage = pw.MemoryImage(logoData.buffer.asUint8List());
|
||||||
|
|
||||||
|
final regularFontData = await rootBundle.load(
|
||||||
|
'assets/fonts/Roboto-Regular.ttf',
|
||||||
|
);
|
||||||
|
final boldFontData = await rootBundle.load('assets/fonts/Roboto-Bold.ttf');
|
||||||
|
final regularFont = pw.Font.ttf(regularFontData);
|
||||||
|
final boldFont = pw.Font.ttf(boldFontData);
|
||||||
|
|
||||||
final doc = pw.Document();
|
final doc = pw.Document();
|
||||||
final created = AppTime.formatDate(task.createdAt);
|
final created = AppTime.formatDate(task.createdAt);
|
||||||
|
|
||||||
|
|
@ -85,30 +94,31 @@ Future<Uint8List> buildTaskPdfBytes(
|
||||||
final profileById = {for (final p in profiles) p.id: p};
|
final profileById = {for (final p in profiles) p.id: p};
|
||||||
final assignedForTask = assignments.where((a) => a.taskId == task.id).toList()
|
final assignedForTask = assignments.where((a) => a.taskId == task.id).toList()
|
||||||
..sort((a, b) => a.createdAt.compareTo(b.createdAt));
|
..sort((a, b) => a.createdAt.compareTo(b.createdAt));
|
||||||
// Collect all unique assigned user IDs for this task and map to profile names
|
|
||||||
final assignedUserIds = {for (final a in assignedForTask) a.userId};
|
final assignedUserIds = {for (final a in assignedForTask) a.userId};
|
||||||
final performedBy = assignedUserIds.isEmpty
|
final performedBy = assignedUserIds.isEmpty
|
||||||
? ''
|
? ''
|
||||||
: assignedUserIds.map((id) => profileById[id]?.fullName ?? id).join(', ');
|
: assignedUserIds.map((id) => profileById[id]?.fullName ?? id).join(', ');
|
||||||
|
|
||||||
|
// Use MultiPage to avoid overflow on long content and apply embedded fonts via ThemeData
|
||||||
doc.addPage(
|
doc.addPage(
|
||||||
pw.Page(
|
pw.MultiPage(
|
||||||
pageFormat: format ?? pdf.PdfPageFormat.a4,
|
pageFormat: format ?? pdf.PdfPageFormat.a4,
|
||||||
margin: pw.EdgeInsets.all(28),
|
margin: pw.EdgeInsets.all(28),
|
||||||
build: (pw.Context ctx) {
|
theme: pw.ThemeData.withFont(base: regularFont, bold: boldFont),
|
||||||
return pw.Column(
|
footer: (pw.Context ctx) => pw.Container(
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
alignment: pw.Alignment.centerRight,
|
||||||
children: [
|
child: pw.Text(
|
||||||
|
'MC-IHO-F-05 Rev. 2',
|
||||||
|
style: pw.TextStyle(fontSize: 9, color: pdf.PdfColors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
build: (pw.Context ctx) => [
|
||||||
pw.Center(
|
pw.Center(
|
||||||
child: pw.Row(
|
child: pw.Row(
|
||||||
mainAxisAlignment: pw.MainAxisAlignment.center,
|
mainAxisAlignment: pw.MainAxisAlignment.center,
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
pw.Container(
|
pw.Container(width: 80, height: 80, child: pw.Image(logoImage)),
|
||||||
width: 80,
|
|
||||||
height: 80,
|
|
||||||
child: pw.Image(logoImage),
|
|
||||||
),
|
|
||||||
pw.SizedBox(width: 16),
|
pw.SizedBox(width: 16),
|
||||||
pw.Column(
|
pw.Column(
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||||
|
|
@ -145,10 +155,7 @@ Future<Uint8List> buildTaskPdfBytes(
|
||||||
pw.Center(
|
pw.Center(
|
||||||
child: pw.Text(
|
child: pw.Text(
|
||||||
'IT Job / Maintenance Request Form',
|
'IT Job / Maintenance Request Form',
|
||||||
style: pw.TextStyle(
|
style: pw.TextStyle(fontSize: 16, fontWeight: pw.FontWeight.bold),
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: pw.FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
pw.SizedBox(height: 12),
|
pw.SizedBox(height: 12),
|
||||||
|
|
@ -399,7 +406,13 @@ Future<Uint8List> buildTaskPdfBytes(
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
pw.SizedBox(height: 36),
|
pw.SizedBox(height: 36),
|
||||||
// Signature lines row (fixed) — stays aligned regardless of name length
|
|
||||||
|
// Wrap signature block to prevent awkward page breaks
|
||||||
|
pw.Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 6,
|
||||||
|
children: [
|
||||||
|
// Signature lines row (fixed)
|
||||||
pw.Row(
|
pw.Row(
|
||||||
children: [
|
children: [
|
||||||
pw.Expanded(
|
pw.Expanded(
|
||||||
|
|
@ -419,8 +432,7 @@ Future<Uint8List> buildTaskPdfBytes(
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
pw.SizedBox(height: 6),
|
// Names row
|
||||||
// Names row: performedBy can be long but won't move the signature line; center names under lines
|
|
||||||
pw.Row(
|
pw.Row(
|
||||||
children: [
|
children: [
|
||||||
pw.Expanded(
|
pw.Expanded(
|
||||||
|
|
@ -448,8 +460,7 @@ Future<Uint8List> buildTaskPdfBytes(
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
pw.SizedBox(height: 6),
|
// Labels row
|
||||||
// Labels row (centered)
|
|
||||||
pw.Row(
|
pw.Row(
|
||||||
children: [
|
children: [
|
||||||
pw.Expanded(
|
pw.Expanded(
|
||||||
|
|
@ -464,18 +475,10 @@ Future<Uint8List> buildTaskPdfBytes(
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
pw.SizedBox(height: 12),
|
|
||||||
pw.Spacer(),
|
|
||||||
pw.Align(
|
|
||||||
alignment: pw.Alignment.centerRight,
|
|
||||||
child: pw.Text(
|
|
||||||
'MC-IHO-F-05 Rev. 2',
|
|
||||||
style: pw.TextStyle(fontSize: 9, color: pdf.PdfColors.grey),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
},
|
pw.SizedBox(height: 12),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -494,32 +497,137 @@ Future<void> showTaskPdfPreview(
|
||||||
) async {
|
) async {
|
||||||
await showDialog<void>(
|
await showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => TaskPdfDialog(
|
||||||
contentPadding: const EdgeInsets.all(8),
|
task: task,
|
||||||
content: SizedBox(
|
ticket: ticket,
|
||||||
width: 700,
|
officeName: officeName,
|
||||||
height: 900,
|
serviceName: serviceName,
|
||||||
child: PdfPreview(
|
logs: logs,
|
||||||
build: (format) => buildTaskPdfBytes(
|
assignments: assignments,
|
||||||
task,
|
profiles: profiles,
|
||||||
ticket,
|
|
||||||
officeName,
|
|
||||||
serviceName,
|
|
||||||
logs,
|
|
||||||
assignments,
|
|
||||||
profiles,
|
|
||||||
format,
|
|
||||||
),
|
|
||||||
allowPrinting: true,
|
|
||||||
allowSharing: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(ctx).pop(),
|
|
||||||
child: const Text('Close'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TaskPdfDialog extends StatefulWidget {
|
||||||
|
final Task task;
|
||||||
|
final Ticket? ticket;
|
||||||
|
final String officeName;
|
||||||
|
final String serviceName;
|
||||||
|
final List<TaskActivityLog> logs;
|
||||||
|
final List<TaskAssignment> assignments;
|
||||||
|
final List<Profile> profiles;
|
||||||
|
|
||||||
|
const TaskPdfDialog({
|
||||||
|
super.key,
|
||||||
|
required this.task,
|
||||||
|
this.ticket,
|
||||||
|
required this.officeName,
|
||||||
|
required this.serviceName,
|
||||||
|
required this.logs,
|
||||||
|
required this.assignments,
|
||||||
|
required this.profiles,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TaskPdfDialog> createState() => _TaskPdfDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TaskPdfDialogState extends State<TaskPdfDialog> {
|
||||||
|
Future<Uint8List>? _pdfFuture;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Initialize once
|
||||||
|
_pdfFuture = buildTaskPdfBytes(
|
||||||
|
widget.task,
|
||||||
|
widget.ticket,
|
||||||
|
widget.officeName,
|
||||||
|
widget.serviceName,
|
||||||
|
widget.logs,
|
||||||
|
widget.assignments,
|
||||||
|
widget.profiles,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
content: SizedBox(
|
||||||
|
width: 800,
|
||||||
|
height: 900,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Expanded(
|
||||||
|
child: Text(
|
||||||
|
'IT Job Preview',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Print',
|
||||||
|
icon: const Icon(Icons.print),
|
||||||
|
onPressed: () async {
|
||||||
|
final bytes = await _pdfFuture;
|
||||||
|
if (bytes == null) return;
|
||||||
|
await Printing.layoutPdf(
|
||||||
|
onLayout: (_) async => bytes,
|
||||||
|
name:
|
||||||
|
'Task - ${widget.task.taskNumber ?? widget.task.id}.pdf',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Download',
|
||||||
|
icon: const Icon(Icons.download),
|
||||||
|
onPressed: () async {
|
||||||
|
final bytes = await _pdfFuture;
|
||||||
|
if (bytes == null) return;
|
||||||
|
await Printing.sharePdf(
|
||||||
|
bytes: bytes,
|
||||||
|
filename:
|
||||||
|
'Task - ${widget.task.taskNumber ?? widget.task.id}.pdf',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Close',
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
Expanded(
|
||||||
|
child: FutureBuilder<Uint8List>(
|
||||||
|
future: _pdfFuture,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return Center(child: Text(snapshot.error.toString()));
|
||||||
|
}
|
||||||
|
final data = snapshot.data;
|
||||||
|
if (data == null) return const SizedBox.shrink();
|
||||||
|
return PdfViewer.data(data, sourceName: 'task.pdf');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
pdfium_flutter
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|
|
||||||
36
pubspec.lock
36
pubspec.lock
|
|
@ -325,10 +325,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: file
|
name: file
|
||||||
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
|
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.4"
|
version: "7.0.1"
|
||||||
file_picker:
|
file_picker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -981,6 +981,38 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.4"
|
||||||
|
pdfium_dart:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pdfium_dart
|
||||||
|
sha256: f1683b9070ddc5c9189c6ee008c285791da66328ce1b882c7162d3393f3a4a74
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.3"
|
||||||
|
pdfium_flutter:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pdfium_flutter
|
||||||
|
sha256: "0c8b7d5d11d20a1486eade599648e907067568955bd14a1b06de076a968b60a1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.9"
|
||||||
|
pdfrx:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: pdfrx
|
||||||
|
sha256: e32e0c786528eec2b3c56b43f59ef1debce3a27c7accd862b95413f949afcfa9
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.24"
|
||||||
|
pdfrx_engine:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pdfrx_engine
|
||||||
|
sha256: a8914433d1f6188b903c53d36b9d7dc908bfa89131591a9db22f1a22470d3a48
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.9"
|
||||||
permission_handler:
|
permission_handler:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ dependencies:
|
||||||
flutter_quill: ^11.5.0
|
flutter_quill: ^11.5.0
|
||||||
file_picker: ^10.3.10
|
file_picker: ^10.3.10
|
||||||
pdf: ^3.11.3
|
pdf: ^3.11.3
|
||||||
|
pdfrx: ^2.2.24
|
||||||
printing: ^5.14.2
|
printing: ^5.14.2
|
||||||
flutter_keyboard_visibility: ^5.4.1
|
flutter_keyboard_visibility: ^5.4.1
|
||||||
awesome_snackbar_content: ^0.1.8
|
awesome_snackbar_content: ^0.1.8
|
||||||
|
|
@ -43,6 +44,7 @@ flutter:
|
||||||
assets:
|
assets:
|
||||||
- .env
|
- .env
|
||||||
- assets/
|
- assets/
|
||||||
|
- assets/fonts/
|
||||||
|
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
android: true
|
android: true
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
flutter_local_notifications_windows
|
flutter_local_notifications_windows
|
||||||
|
pdfium_flutter
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user