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:audioplayers/audioplayers.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pdfrx/pdfrx.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
|
|
@ -36,6 +37,8 @@ Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
|||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
// The flag optionally hides annoying WASM warnings in your Chrome dev console
|
||||
pdfrxFlutterInitialize(dismissPdfiumWasmWarnings: true);
|
||||
|
||||
// initialize Firebase before anything that uses messaging
|
||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||
|
|
@ -192,7 +195,10 @@ class NotificationSoundObserver extends ProviderObserver {
|
|||
controller.registerFcmToken(token);
|
||||
}
|
||||
})
|
||||
.catchError((e) => debugPrint('getToken error: $e'));
|
||||
.catchError((e) {
|
||||
debugPrint('getToken error: $e');
|
||||
return null;
|
||||
});
|
||||
_tokenSub = FirebaseMessaging.instance.onTokenRefresh.listen((token) {
|
||||
debugPrint('token refreshed: $token');
|
||||
controller.registerFcmToken(token);
|
||||
|
|
@ -210,7 +216,10 @@ class NotificationSoundObserver extends ProviderObserver {
|
|||
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/pdf.dart' as pdf;
|
||||
import 'package:printing/printing.dart';
|
||||
import 'package:pdfrx/pdfrx.dart';
|
||||
|
||||
import '../../models/task.dart';
|
||||
import '../../models/ticket.dart';
|
||||
|
|
@ -27,6 +28,14 @@ Future<Uint8List> buildTaskPdfBytes(
|
|||
) async {
|
||||
final logoData = await rootBundle.load('assets/crmc_logo.png');
|
||||
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 created = AppTime.formatDate(task.createdAt);
|
||||
|
||||
|
|
@ -85,321 +94,325 @@ Future<Uint8List> buildTaskPdfBytes(
|
|||
final profileById = {for (final p in profiles) p.id: p};
|
||||
final assignedForTask = assignments.where((a) => a.taskId == task.id).toList()
|
||||
..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 performedBy = assignedUserIds.isEmpty
|
||||
? ''
|
||||
: assignedUserIds.map((id) => profileById[id]?.fullName ?? id).join(', ');
|
||||
|
||||
// Use MultiPage to avoid overflow on long content and apply embedded fonts via ThemeData
|
||||
doc.addPage(
|
||||
pw.Page(
|
||||
pw.MultiPage(
|
||||
pageFormat: format ?? pdf.PdfPageFormat.a4,
|
||||
margin: pw.EdgeInsets.all(28),
|
||||
build: (pw.Context ctx) {
|
||||
return pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Center(
|
||||
child: pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.center,
|
||||
theme: pw.ThemeData.withFont(base: regularFont, bold: boldFont),
|
||||
footer: (pw.Context ctx) => pw.Container(
|
||||
alignment: pw.Alignment.centerRight,
|
||||
child: pw.Text(
|
||||
'MC-IHO-F-05 Rev. 2',
|
||||
style: pw.TextStyle(fontSize: 9, color: pdf.PdfColors.grey),
|
||||
),
|
||||
),
|
||||
build: (pw.Context ctx) => [
|
||||
pw.Center(
|
||||
child: pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.center,
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||
children: [
|
||||
pw.Container(width: 80, height: 80, child: pw.Image(logoImage)),
|
||||
pw.SizedBox(width: 16),
|
||||
pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||
children: [
|
||||
pw.Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: pw.Image(logoImage),
|
||||
pw.Text(
|
||||
'Republic of the Philippines',
|
||||
textAlign: pw.TextAlign.center,
|
||||
),
|
||||
pw.SizedBox(width: 16),
|
||||
pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||
pw.Text(
|
||||
'Department of Health',
|
||||
textAlign: pw.TextAlign.center,
|
||||
),
|
||||
pw.Text(
|
||||
'Regional and Medical Center',
|
||||
textAlign: pw.TextAlign.center,
|
||||
),
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Text(
|
||||
'Cotabato Regional and Medical Center',
|
||||
textAlign: pw.TextAlign.center,
|
||||
style: pw.TextStyle(fontWeight: pw.FontWeight.bold),
|
||||
),
|
||||
pw.Text(
|
||||
'Integrated Hospital Operations and Management Program',
|
||||
textAlign: pw.TextAlign.center,
|
||||
),
|
||||
pw.Text('(IHOMP)', textAlign: pw.TextAlign.center),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
pw.SizedBox(height: 12),
|
||||
pw.Center(
|
||||
child: pw.Text(
|
||||
'IT Job / Maintenance Request Form',
|
||||
style: pw.TextStyle(fontSize: 16, fontWeight: pw.FontWeight.bold),
|
||||
),
|
||||
),
|
||||
pw.SizedBox(height: 12),
|
||||
pw.Row(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Expanded(
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Text(
|
||||
'Republic of the Philippines',
|
||||
textAlign: pw.TextAlign.center,
|
||||
pw.Text('Task Number: '),
|
||||
pw.Container(
|
||||
padding: pw.EdgeInsets.only(bottom: 2),
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border(
|
||||
bottom: pw.BorderSide(
|
||||
width: 0.8,
|
||||
color: pdf.PdfColors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: pw.Text(task.taskNumber ?? task.id),
|
||||
),
|
||||
pw.Text(
|
||||
'Department of Health',
|
||||
textAlign: pw.TextAlign.center,
|
||||
],
|
||||
),
|
||||
pw.SizedBox(height: 8),
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Text('Service: '),
|
||||
pw.Container(
|
||||
padding: pw.EdgeInsets.only(bottom: 2),
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border(
|
||||
bottom: pw.BorderSide(
|
||||
width: 0.8,
|
||||
color: pdf.PdfColors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: pw.Text(serviceName),
|
||||
),
|
||||
pw.Text(
|
||||
'Regional and Medical Center',
|
||||
textAlign: pw.TextAlign.center,
|
||||
],
|
||||
),
|
||||
pw.SizedBox(height: 8),
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Text('Type: '),
|
||||
pw.Container(
|
||||
padding: pw.EdgeInsets.only(bottom: 2),
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border(
|
||||
bottom: pw.BorderSide(
|
||||
width: 0.8,
|
||||
color: pdf.PdfColors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: pw.Text(task.requestType ?? ''),
|
||||
),
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Text(
|
||||
'Cotabato Regional and Medical Center',
|
||||
textAlign: pw.TextAlign.center,
|
||||
style: pw.TextStyle(fontWeight: pw.FontWeight.bold),
|
||||
),
|
||||
pw.Text(
|
||||
'Integrated Hospital Operations and Management Program',
|
||||
textAlign: pw.TextAlign.center,
|
||||
),
|
||||
pw.Text('(IHOMP)', textAlign: pw.TextAlign.center),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
pw.SizedBox(height: 12),
|
||||
pw.Center(
|
||||
child: pw.Text(
|
||||
'IT Job / Maintenance Request Form',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
pw.SizedBox(width: 12),
|
||||
pw.Container(
|
||||
width: 180,
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Text('Filed At: '),
|
||||
pw.Container(
|
||||
padding: pw.EdgeInsets.only(bottom: 2),
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border(
|
||||
bottom: pw.BorderSide(
|
||||
width: 0.8,
|
||||
color: pdf.PdfColors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: pw.Text(created),
|
||||
),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(height: 8),
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Text('Office: '),
|
||||
pw.Container(
|
||||
padding: pw.EdgeInsets.only(bottom: 2),
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border(
|
||||
bottom: pw.BorderSide(
|
||||
width: 0.8,
|
||||
color: pdf.PdfColors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: pw.Text(officeName),
|
||||
),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(height: 8),
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Text('Category: '),
|
||||
pw.Container(
|
||||
padding: pw.EdgeInsets.only(bottom: 2),
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border(
|
||||
bottom: pw.BorderSide(
|
||||
width: 0.8,
|
||||
color: pdf.PdfColors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: pw.Text(task.requestCategory ?? ''),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(height: 12),
|
||||
pw.Divider(thickness: 0.8, color: pdf.PdfColors.grey),
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Center(
|
||||
child: pw.Text(
|
||||
task.title,
|
||||
textAlign: pw.TextAlign.center,
|
||||
style: pw.TextStyle(fontWeight: pw.FontWeight.bold),
|
||||
),
|
||||
),
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Divider(thickness: 0.8, color: pdf.PdfColors.grey),
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Text('Description:'),
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Container(
|
||||
padding: pw.EdgeInsets.all(6),
|
||||
child: pw.Text(descriptionText),
|
||||
),
|
||||
pw.SizedBox(height: 12),
|
||||
// Requested/Noted signature lines (bottom-aligned to match Performed/Received)
|
||||
pw.Row(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
||||
children: [
|
||||
pw.Expanded(
|
||||
child: pw.Container(
|
||||
height: 56,
|
||||
child: pw.Column(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.end,
|
||||
children: [
|
||||
pw.Container(height: 1, color: pdf.PdfColors.black),
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Text(
|
||||
requestedBy,
|
||||
style: pw.TextStyle(fontWeight: pw.FontWeight.bold),
|
||||
),
|
||||
pw.Text('Requested By'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
pw.SizedBox(height: 12),
|
||||
pw.Row(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Expanded(
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Text('Task Number: '),
|
||||
pw.Container(
|
||||
padding: pw.EdgeInsets.only(bottom: 2),
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border(
|
||||
bottom: pw.BorderSide(
|
||||
width: 0.8,
|
||||
color: pdf.PdfColors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: pw.Text(task.taskNumber ?? task.id),
|
||||
),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(height: 8),
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Text('Service: '),
|
||||
pw.Container(
|
||||
padding: pw.EdgeInsets.only(bottom: 2),
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border(
|
||||
bottom: pw.BorderSide(
|
||||
width: 0.8,
|
||||
color: pdf.PdfColors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: pw.Text(serviceName),
|
||||
),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(height: 8),
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Text('Type: '),
|
||||
pw.Container(
|
||||
padding: pw.EdgeInsets.only(bottom: 2),
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border(
|
||||
bottom: pw.BorderSide(
|
||||
width: 0.8,
|
||||
color: pdf.PdfColors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: pw.Text(task.requestType ?? ''),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(width: 12),
|
||||
pw.Expanded(
|
||||
child: pw.Container(
|
||||
height: 56,
|
||||
child: pw.Column(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.end,
|
||||
children: [
|
||||
pw.Container(height: 1, color: pdf.PdfColors.black),
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Text(
|
||||
notedBy,
|
||||
style: pw.TextStyle(fontWeight: pw.FontWeight.bold),
|
||||
),
|
||||
pw.Text('Noted by Supervisor/Senior'),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(width: 12),
|
||||
pw.Container(
|
||||
width: 180,
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Text('Filed At: '),
|
||||
pw.Container(
|
||||
padding: pw.EdgeInsets.only(bottom: 2),
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border(
|
||||
bottom: pw.BorderSide(
|
||||
width: 0.8,
|
||||
color: pdf.PdfColors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: pw.Text(created),
|
||||
),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(height: 8),
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Text('Office: '),
|
||||
pw.Container(
|
||||
padding: pw.EdgeInsets.only(bottom: 2),
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border(
|
||||
bottom: pw.BorderSide(
|
||||
width: 0.8,
|
||||
color: pdf.PdfColors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: pw.Text(officeName),
|
||||
),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(height: 8),
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Text('Category: '),
|
||||
pw.Container(
|
||||
padding: pw.EdgeInsets.only(bottom: 2),
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border(
|
||||
bottom: pw.BorderSide(
|
||||
width: 0.8,
|
||||
color: pdf.PdfColors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: pw.Text(task.requestCategory ?? ''),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(height: 12),
|
||||
pw.Divider(thickness: 0.8, color: pdf.PdfColors.grey),
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Center(
|
||||
child: pw.Text(
|
||||
task.title,
|
||||
textAlign: pw.TextAlign.center,
|
||||
style: pw.TextStyle(fontWeight: pw.FontWeight.bold),
|
||||
),
|
||||
),
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Divider(thickness: 0.8, color: pdf.PdfColors.grey),
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Text('Description:'),
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Container(
|
||||
padding: pw.EdgeInsets.all(6),
|
||||
child: pw.Text(descriptionText),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(height: 12),
|
||||
pw.Text('Action Taken:'),
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Container(
|
||||
padding: pw.EdgeInsets.all(6),
|
||||
child: pw.Text(actionTakenText),
|
||||
),
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Divider(thickness: 0.8, color: pdf.PdfColors.grey),
|
||||
pw.SizedBox(height: 12),
|
||||
// Historical timestamps side-by-side: Created / Started / Closed
|
||||
pw.Row(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Expanded(
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Text('Created At:'),
|
||||
pw.SizedBox(height: 4),
|
||||
pw.Text(
|
||||
'${AppTime.formatDate(task.createdAt)} ${AppTime.formatTime(task.createdAt)}',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
pw.SizedBox(height: 12),
|
||||
// Requested/Noted signature lines (bottom-aligned to match Performed/Received)
|
||||
pw.Row(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
||||
children: [
|
||||
pw.Expanded(
|
||||
child: pw.Container(
|
||||
height: 56,
|
||||
child: pw.Column(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.end,
|
||||
children: [
|
||||
pw.Container(height: 1, color: pdf.PdfColors.black),
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Text(
|
||||
requestedBy,
|
||||
style: pw.TextStyle(fontWeight: pw.FontWeight.bold),
|
||||
),
|
||||
pw.Text('Requested By'),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(width: 12),
|
||||
pw.Expanded(
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Text('Started At:'),
|
||||
pw.SizedBox(height: 4),
|
||||
pw.Text(
|
||||
task.startedAt == null
|
||||
? ''
|
||||
: '${AppTime.formatDate(task.startedAt!)} ${AppTime.formatTime(task.startedAt!)}',
|
||||
),
|
||||
),
|
||||
pw.SizedBox(width: 12),
|
||||
pw.Expanded(
|
||||
child: pw.Container(
|
||||
height: 56,
|
||||
child: pw.Column(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.end,
|
||||
children: [
|
||||
pw.Container(height: 1, color: pdf.PdfColors.black),
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Text(
|
||||
notedBy,
|
||||
style: pw.TextStyle(fontWeight: pw.FontWeight.bold),
|
||||
),
|
||||
pw.Text('Noted by Supervisor/Senior'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
pw.SizedBox(height: 12),
|
||||
pw.Text('Action Taken:'),
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Container(
|
||||
padding: pw.EdgeInsets.all(6),
|
||||
child: pw.Text(actionTakenText),
|
||||
pw.SizedBox(width: 12),
|
||||
pw.Expanded(
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Text('Closed At:'),
|
||||
pw.SizedBox(height: 4),
|
||||
pw.Text(
|
||||
task.completedAt == null
|
||||
? ''
|
||||
: '${AppTime.formatDate(task.completedAt!)} ${AppTime.formatTime(task.completedAt!)}',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Divider(thickness: 0.8, color: pdf.PdfColors.grey),
|
||||
pw.SizedBox(height: 12),
|
||||
// Historical timestamps side-by-side: Created / Started / Closed
|
||||
pw.Row(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Expanded(
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Text('Created At:'),
|
||||
pw.SizedBox(height: 4),
|
||||
pw.Text(
|
||||
'${AppTime.formatDate(task.createdAt)} ${AppTime.formatTime(task.createdAt)}',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
pw.SizedBox(width: 12),
|
||||
pw.Expanded(
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Text('Started At:'),
|
||||
pw.SizedBox(height: 4),
|
||||
pw.Text(
|
||||
task.startedAt == null
|
||||
? ''
|
||||
: '${AppTime.formatDate(task.startedAt!)} ${AppTime.formatTime(task.startedAt!)}',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
pw.SizedBox(width: 12),
|
||||
pw.Expanded(
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Text('Closed At:'),
|
||||
pw.SizedBox(height: 4),
|
||||
pw.Text(
|
||||
task.completedAt == null
|
||||
? ''
|
||||
: '${AppTime.formatDate(task.completedAt!)} ${AppTime.formatTime(task.completedAt!)}',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(height: 36),
|
||||
// Signature lines row (fixed) — stays aligned regardless of name length
|
||||
],
|
||||
),
|
||||
pw.SizedBox(height: 36),
|
||||
|
||||
// Wrap signature block to prevent awkward page breaks
|
||||
pw.Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
// Signature lines row (fixed)
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Expanded(
|
||||
|
|
@ -419,8 +432,7 @@ Future<Uint8List> buildTaskPdfBytes(
|
|||
),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(height: 6),
|
||||
// Names row: performedBy can be long but won't move the signature line; center names under lines
|
||||
// Names row
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Expanded(
|
||||
|
|
@ -448,8 +460,7 @@ Future<Uint8List> buildTaskPdfBytes(
|
|||
),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(height: 6),
|
||||
// Labels row (centered)
|
||||
// Labels row
|
||||
pw.Row(
|
||||
children: [
|
||||
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 {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
contentPadding: const EdgeInsets.all(8),
|
||||
content: SizedBox(
|
||||
width: 700,
|
||||
height: 900,
|
||||
child: PdfPreview(
|
||||
build: (format) => buildTaskPdfBytes(
|
||||
task,
|
||||
ticket,
|
||||
officeName,
|
||||
serviceName,
|
||||
logs,
|
||||
assignments,
|
||||
profiles,
|
||||
format,
|
||||
),
|
||||
allowPrinting: true,
|
||||
allowSharing: true,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: const Text('Close'),
|
||||
),
|
||||
],
|
||||
builder: (ctx) => TaskPdfDialog(
|
||||
task: task,
|
||||
ticket: ticket,
|
||||
officeName: officeName,
|
||||
serviceName: serviceName,
|
||||
logs: logs,
|
||||
assignments: assignments,
|
||||
profiles: profiles,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
pdfium_flutter
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
|
|
|||
36
pubspec.lock
36
pubspec.lock
|
|
@ -325,10 +325,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
version: "7.0.1"
|
||||
file_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -981,6 +981,38 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ dependencies:
|
|||
flutter_quill: ^11.5.0
|
||||
file_picker: ^10.3.10
|
||||
pdf: ^3.11.3
|
||||
pdfrx: ^2.2.24
|
||||
printing: ^5.14.2
|
||||
flutter_keyboard_visibility: ^5.4.1
|
||||
awesome_snackbar_content: ^0.1.8
|
||||
|
|
@ -43,6 +44,7 @@ flutter:
|
|||
assets:
|
||||
- .env
|
||||
- assets/
|
||||
- assets/fonts/
|
||||
|
||||
flutter_launcher_icons:
|
||||
android: true
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
flutter_local_notifications_windows
|
||||
pdfium_flutter
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user