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/it_service_request.dart'; import '../../models/it_service_request_assignment.dart'; import '../../models/office.dart'; import '../../models/profile.dart'; import '../../utils/app_time.dart'; /// Build PDF bytes for IT Service Request Form. Future buildItServiceRequestPdfBytes({ required ItServiceRequest request, required List assignments, required Map profileById, required Map officeById, 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 officeName = request.officeId != null ? officeById[request.officeId]?.name ?? '' : ''; final assignedStaff = assignments .map((a) => profileById[a.userId]?.fullName ?? a.userId) .toList(); final eventDetailsText = _plainFromDelta(request.eventDetails); final remarksText = _plainFromDelta(request.remarks); final selectedServices = request.services; final othersText = request.servicesOther ?? ''; final eventNameWithDetails = eventDetailsText.isEmpty ? request.eventName : '${request.eventName}: $eventDetailsText'; final dateTimeReceivedStr = request.dateTimeReceived != null ? '${AppTime.formatDate(request.dateTimeReceived!)} ${AppTime.formatTime(request.dateTimeReceived!)}' : ''; final dateTimeCheckedStr = request.dateTimeChecked != null ? '${AppTime.formatDate(request.dateTimeChecked!)} ${AppTime.formatTime(request.dateTimeChecked!)}' : ''; final eventDateStr = request.eventDate != null ? '${AppTime.formatDate(request.eventDate!)} ${AppTime.formatTime(request.eventDate!)}' : ''; final eventEndStr = request.eventEndDate != null ? ' to ${AppTime.formatDate(request.eventEndDate!)} ${AppTime.formatTime(request.eventEndDate!)}' : ''; final dryRunDateStr = request.dryRunDate != null ? '${AppTime.formatDate(request.dryRunDate!)} ${AppTime.formatTime(request.dryRunDate!)}' : ''; final dryRunEndStr = request.dryRunEndDate != null ? ' to ${AppTime.formatDate(request.dryRunEndDate!)} ${AppTime.formatTime(request.dryRunEndDate!)}' : ''; final smallStyle = pw.TextStyle(fontSize: 8); final labelStyle = pw.TextStyle(fontSize: 10); final boldLabelStyle = pw.TextStyle( fontSize: 10, fontWeight: pw.FontWeight.bold, ); final headerItalicStyle = pw.TextStyle( fontSize: 10, fontStyle: pw.FontStyle.italic, ); final headerBoldStyle = pw.TextStyle( fontSize: 11, fontWeight: pw.FontWeight.bold, ); doc.addPage( pw.MultiPage( pageFormat: format ?? pdf.PdfPageFormat.a4, margin: const pw.EdgeInsets.symmetric(horizontal: 40, vertical: 28), theme: pw.ThemeData.withFont( base: regularFont, bold: boldFont, italic: regularFont, boldItalic: boldFont, ), footer: (pw.Context ctx) => pw.Container( alignment: pw.Alignment.centerRight, child: pw.Text('MC-IHO-F-17 Rev. 0', style: pw.TextStyle(fontSize: 8)), ), build: (pw.Context ctx) => [ // ── Header ── pw.Row( mainAxisAlignment: pw.MainAxisAlignment.center, crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ pw.Container(width: 64, height: 64, child: pw.Image(logoImage)), pw.SizedBox(width: 12), pw.Column( mainAxisSize: pw.MainAxisSize.min, crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ pw.Text( 'Republic of the Philippines', style: headerItalicStyle, ), pw.Text('Department of Health', style: headerItalicStyle), pw.Text( 'COTABATO REGIONAL AND MEDICAL CENTER', style: headerBoldStyle, ), pw.SizedBox(height: 6), pw.Text( 'INTEGRATED HOSPITAL OPERATIONS AND MANAGEMENT PROGRAM', style: pw.TextStyle( fontSize: 9, fontWeight: pw.FontWeight.bold, ), ), pw.Text( 'IHOMP', style: pw.TextStyle( fontSize: 9, fontWeight: pw.FontWeight.bold, ), ), ], ), ], ), pw.SizedBox(height: 14), // ── Title ── pw.Center( child: pw.Text( 'IT SERVICE REQUEST FORM', style: pw.TextStyle(fontSize: 13, fontWeight: pw.FontWeight.bold), ), ), pw.SizedBox(height: 14), // ── Note + Date/Time Received/Checked ── pw.Row( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Expanded( flex: 3, child: pw.Text( '* Ensure availability of venue, power supply, sound system, ' 'microphone, power point presentation, videos, music and ' 'other necessary files needed for the event of activity.', style: pw.TextStyle( fontSize: 8, fontStyle: pw.FontStyle.italic, ), ), ), pw.SizedBox(width: 16), pw.Expanded( flex: 2, child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ _underlineField( 'Date/Time Received:', dateTimeReceivedStr, style: labelStyle, ), pw.SizedBox(height: 6), _underlineField( 'Date/Time Checked:', dateTimeCheckedStr, style: labelStyle, ), ], ), ), ], ), pw.SizedBox(height: 14), // ── Services ── pw.Text('Services', style: boldLabelStyle), pw.SizedBox(height: 6), // Row 1: FB Live Stream, Technical Assistance, Others pw.Row( children: [ pw.Expanded( child: _checkbox( 'FB Live Stream', selectedServices.contains(ItServiceType.fbLiveStream), style: labelStyle, ), ), pw.Expanded( child: _checkbox( 'Technical Assistance', selectedServices.contains(ItServiceType.technicalAssistance), style: labelStyle, ), ), pw.Expanded( child: _checkbox( 'Others${othersText.isNotEmpty ? ' ($othersText)' : ''}', selectedServices.contains(ItServiceType.others), style: labelStyle, ), ), ], ), pw.SizedBox(height: 2), // Row 2: Video Recording, WiFi pw.Row( children: [ pw.Expanded( child: _checkbox( 'Video Recording', selectedServices.contains(ItServiceType.videoRecording), style: labelStyle, ), ), pw.Expanded( child: _checkbox( 'WiFi', selectedServices.contains(ItServiceType.wifi), style: labelStyle, ), ), pw.Expanded(child: pw.SizedBox()), ], ), pw.SizedBox(height: 14), // ── Event/Activity Details ── pw.Text('Event/Activity Details', style: boldLabelStyle), _underlineField('Event Name', eventNameWithDetails, style: labelStyle), pw.SizedBox(height: 14), // ── 4-column: Event Date/Time, Dry Run, Contact Person, Contact Number ── pw.Row( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Expanded( child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text('Event Date and Time', style: smallStyle), pw.SizedBox(height: 4), _underlinedText( '$eventDateStr$eventEndStr', style: smallStyle, ), ], ), ), pw.SizedBox(width: 8), pw.Expanded( child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text('Dry Run Date and Time', style: smallStyle), pw.SizedBox(height: 4), _underlinedText( '$dryRunDateStr$dryRunEndStr', style: smallStyle, ), ], ), ), pw.SizedBox(width: 8), pw.Expanded( child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text('Contact Person', style: smallStyle), pw.SizedBox(height: 4), _underlinedText( request.contactPerson ?? '', style: smallStyle, ), ], ), ), pw.SizedBox(width: 8), pw.Expanded( child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text('Contact Number', style: smallStyle), pw.SizedBox(height: 4), _underlinedText( request.contactNumber ?? '', style: smallStyle, ), ], ), ), ], ), pw.SizedBox(height: 14), // ── IT Staff/s Assigned ── pw.Text('IT Staff/s Assigned', style: boldLabelStyle), pw.SizedBox(height: 4), // Show each staff on a separate underlined row, or empty lines if (assignedStaff.isNotEmpty) ...assignedStaff.map( (name) => pw.Padding( padding: const pw.EdgeInsets.only(bottom: 4), child: _underlinedText(name, style: labelStyle), ), ) else ...[ _underlinedText('', style: labelStyle), pw.SizedBox(height: 4), _underlinedText('', style: labelStyle), ], pw.SizedBox(height: 14), // ── Remarks ── pw.Text('Remarks:', style: boldLabelStyle), pw.SizedBox(height: 4), pw.Container( width: double.infinity, constraints: const pw.BoxConstraints(minHeight: 60), padding: const pw.EdgeInsets.all(4), child: pw.Text(remarksText, style: labelStyle), ), pw.SizedBox(height: 28), // ── Signature blocks ── pw.Row( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ // Left: Requested by pw.Expanded( child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text('Requested by:', style: labelStyle), pw.SizedBox(height: 36), pw.Container( width: double.infinity, decoration: const pw.BoxDecoration( border: pw.Border(bottom: pw.BorderSide(width: 0.8)), ), padding: const pw.EdgeInsets.only(bottom: 2), child: pw.Center( child: pw.Text( request.requestedBy ?? '', style: boldLabelStyle, ), ), ), pw.SizedBox(height: 2), pw.Center( child: pw.Text( 'Signature over printed name', style: smallStyle, ), ), pw.SizedBox(height: 10), _underlineField('Department:', officeName, style: labelStyle), pw.SizedBox(height: 6), _underlineField( 'Date:', AppTime.formatDate(request.createdAt), style: labelStyle, ), ], ), ), pw.SizedBox(width: 40), // Right: Approved by pw.Expanded( child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text('Approved by:', style: labelStyle), pw.SizedBox(height: 36), pw.Container( width: double.infinity, decoration: const pw.BoxDecoration( border: pw.Border(bottom: pw.BorderSide(width: 0.8)), ), padding: const pw.EdgeInsets.only(bottom: 2), child: pw.Center( child: pw.Text( request.approvedBy ?? '', style: boldLabelStyle, ), ), ), pw.SizedBox(height: 2), pw.Center( child: pw.Text('IHOMP \u2013 Head', style: smallStyle), ), pw.SizedBox(height: 10), _underlineField( 'Date:', request.approvedAt != null ? AppTime.formatDate(request.approvedAt!) : '', style: labelStyle, ), ], ), ), ], ), pw.SizedBox(height: 12), ], ), ); return doc.save(); } /// A checkbox with label, matching the form layout. pw.Widget _checkbox(String label, bool checked, {pw.TextStyle? style}) { return pw.Row( mainAxisSize: pw.MainAxisSize.min, children: [ pw.Container( width: 10, height: 10, decoration: pw.BoxDecoration(border: pw.Border.all(width: 0.8)), child: checked ? pw.Center( child: pw.Text( 'X', style: pw.TextStyle( fontSize: 7, fontWeight: pw.FontWeight.bold, ), ), ) : null, ), pw.SizedBox(width: 4), pw.Text(label, style: style), ], ); } /// A label followed by an underlined value, e.g. "Date/Time Received: ____" pw.Widget _underlineField(String label, String value, {pw.TextStyle? style}) { return pw.Row( crossAxisAlignment: pw.CrossAxisAlignment.end, children: [ pw.Text(label, style: style), pw.SizedBox(width: 4), pw.Expanded( child: pw.Container( decoration: const pw.BoxDecoration( border: pw.Border(bottom: pw.BorderSide(width: 0.8)), ), padding: const pw.EdgeInsets.only(bottom: 2), child: pw.Text(value, style: style), ), ), ], ); } /// Text with an underline spanning the full width. pw.Widget _underlinedText(String value, {pw.TextStyle? style}) { return pw.Container( width: double.infinity, decoration: const pw.BoxDecoration( border: pw.Border(bottom: pw.BorderSide(width: 0.5)), ), padding: const pw.EdgeInsets.only(bottom: 2), child: pw.Text(value, style: style), ); } String _plainFromDelta(String? deltaJson) { if (deltaJson == null || deltaJson.trim().isEmpty) return ''; dynamic decoded = deltaJson; 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); } } } 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(); } /// Generate and share/print the IT Service Request PDF. Future generateItServiceRequestPdf({ required BuildContext context, required ItServiceRequest request, required List assignments, required Map profileById, required Map officeById, Uint8List? prebuiltBytes, }) async { final bytes = prebuiltBytes ?? await buildItServiceRequestPdfBytes( request: request, assignments: assignments, profileById: profileById, officeById: officeById, ); await Printing.layoutPdf( onLayout: (_) async => bytes, name: 'ISR-${request.requestNumber ?? request.id}.pdf', ); }