import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show rootBundle; 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'; import '../../models/task_activity_log.dart'; import '../../models/task_assignment.dart'; import '../../models/profile.dart'; import '../../utils/app_time.dart'; Future buildTaskPdfBytes( Task task, Ticket? ticket, String officeName, String serviceName, List logs, List assignments, List profiles, pdf.PdfPageFormat? format, ) 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); final descriptionText = ticket?.description ?? task.description; String plainFromAction(String? at) { if (at == null || at.trim().isEmpty) return ''; dynamic decoded = at; for (var i = 0; i < 3; i++) { if (decoded is String) { try { decoded = jsonDecode(decoded); continue; } catch (_) { break; } } break; } if (decoded is Map && decoded['ops'] is List) { final ops = decoded['ops'] as List; final buf = StringBuffer(); for (final op in ops) { if (op is Map) { final insert = op['insert']; if (insert is String) { buf.write(insert); } else if (insert is Map && insert.containsKey('image')) { buf.write('[image]'); } else { buf.write(insert?.toString() ?? ''); } } else { buf.write(op.toString()); } } return buf.toString().trim(); } if (decoded is List) { try { final doc = quill.Document.fromJson(decoded); return doc.toPlainText().trim(); } catch (_) { return decoded.join(); } } return decoded.toString(); } final actionTakenText = plainFromAction(task.actionTaken); final requestedBy = task.requestedBy ?? ''; final notedBy = task.notedBy ?? ''; final receivedBy = task.receivedBy ?? ''; 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)); 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.MultiPage( pageFormat: format ?? pdf.PdfPageFormat.a4, margin: pw.EdgeInsets.all(28), 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.Text( 'Republic of the Philippines', textAlign: pw.TextAlign.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('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.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(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(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), // Wrap signature block to prevent awkward page breaks pw.Wrap( spacing: 6, runSpacing: 6, children: [ // Signature lines row (fixed) pw.Row( children: [ pw.Expanded( child: pw.Container( height: 24, alignment: pw.Alignment.center, child: pw.Container(height: 1, color: pdf.PdfColors.black), ), ), pw.SizedBox(width: 12), pw.Expanded( child: pw.Container( height: 24, alignment: pw.Alignment.center, child: pw.Container(height: 1, color: pdf.PdfColors.black), ), ), ], ), // Names row pw.Row( children: [ pw.Expanded( child: pw.Container( padding: pw.EdgeInsets.only(right: 6), alignment: pw.Alignment.center, child: pw.Text( performedBy, textAlign: pw.TextAlign.center, style: pw.TextStyle(fontWeight: pw.FontWeight.bold), ), ), ), pw.SizedBox(width: 12), pw.Expanded( child: pw.Container( padding: pw.EdgeInsets.only(left: 6), alignment: pw.Alignment.center, child: pw.Text( receivedBy, textAlign: pw.TextAlign.center, style: pw.TextStyle(fontWeight: pw.FontWeight.bold), ), ), ), ], ), // Labels row pw.Row( children: [ pw.Expanded( child: pw.Text( 'Performed By', textAlign: pw.TextAlign.center, ), ), pw.SizedBox(width: 12), pw.Expanded( child: pw.Text('Received By', textAlign: pw.TextAlign.center), ), ], ), ], ), pw.SizedBox(height: 12), ], ), ); return doc.save(); } Future showTaskPdfPreview( BuildContext context, Task task, Ticket? ticket, String officeName, String serviceName, List logs, List assignments, List profiles, ) async { await showDialog( context: context, 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 logs; final List assignments; final List 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 createState() => _TaskPdfDialogState(); } class _TaskPdfDialogState extends State { Future? _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( 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'); }, ), ), ], ), ), ); } }