549 lines
18 KiB
Dart
549 lines
18 KiB
Dart
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<Uint8List> buildItServiceRequestPdfBytes({
|
|
required ItServiceRequest request,
|
|
required List<ItServiceRequestAssignment> assignments,
|
|
required Map<String, Profile> profileById,
|
|
required Map<String, Office> 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<void> generateItServiceRequestPdf({
|
|
required BuildContext context,
|
|
required ItServiceRequest request,
|
|
required List<ItServiceRequestAssignment> assignments,
|
|
required Map<String, Profile> profileById,
|
|
required Map<String, Office> 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',
|
|
);
|
|
}
|