tasq/lib/screens/tasks/task_pdf.dart

526 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/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<Uint8List> buildTaskPdfBytes(
Task task,
Ticket? ticket,
String officeName,
String serviceName,
List<TaskActivityLog> logs,
List<TaskAssignment> assignments,
List<Profile> profiles,
pdf.PdfPageFormat? format,
) async {
final logoData = await rootBundle.load('assets/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<void> showTaskPdfPreview(
BuildContext context,
Task task,
Ticket? ticket,
String officeName,
String serviceName,
List<TaskActivityLog> logs,
List<TaskAssignment> assignments,
List<Profile> profiles,
) 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'),
),
],
),
);
}