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 '../../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('crmc_logo.png'); final logoImage = pw.MemoryImage(logoData.buffer.asUint8List()); 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)); // 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(', '); doc.addPage( pw.Page( 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, 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), // Signature lines row (fixed) — stays aligned regardless of name length 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), ), ), ], ), pw.SizedBox(height: 6), // Names row: performedBy can be long but won't move the signature line; center names under lines 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), ), ), ), ], ), pw.SizedBox(height: 6), // Labels row (centered) 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), 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), ), ), ], ); }, ), ); 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) => 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'), ), ], ), ); }