Files

322 lines
11 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
// ─── DTOs ─────────────────────────────────────────────────────────────────────
class WorkLogEntryDto {
const WorkLogEntryDto({
required this.type,
required this.title,
this.subtitle,
required this.start,
this.end,
required this.isCreatorEvent,
required this.duration,
});
// 'attendance' | 'task' | 'ticket' | 'isr' | 'passSlip' | 'vacant'
final String type;
final String title;
final String? subtitle;
final DateTime start;
final DateTime? end;
final bool isCreatorEvent;
final Duration duration;
}
class WorkLogSummaryDto {
const WorkLogSummaryDto({
required this.totalAttendance,
required this.totalTasks,
required this.totalTickets,
required this.totalIsrs,
required this.totalPassSlip,
required this.totalVacant,
});
final Duration totalAttendance;
final Duration totalTasks;
final Duration totalTickets;
final Duration totalIsrs;
final Duration totalPassSlip;
final Duration totalVacant;
Duration get totalActive =>
totalAttendance + totalTasks + totalTickets + totalIsrs + totalPassSlip;
}
class DailyWorkLogDto {
const DailyWorkLogDto({
required this.date,
required this.entries,
required this.summary,
});
final DateTime date;
final List<WorkLogEntryDto> entries;
final WorkLogSummaryDto summary;
bool get hasData => entries.any((e) => !e.isCreatorEvent);
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
String _fmt(Duration d) {
if (d.inMinutes < 1) return '< 1m';
final h = d.inHours;
final m = d.inMinutes % 60;
return h > 0 ? '${h}h ${m}m' : '${m}m';
}
String _fmtTime(DateTime dt) => DateFormat('h:mm a').format(dt);
Future<pw.Font> _font(String path) async {
final data = await rootBundle.load(path);
return pw.Font.ttf(data);
}
String _typeLabel(String type) => switch (type) {
'attendance' => 'Attendance',
'task' => 'Task',
'ticket' => 'Ticket',
'isr' => 'ISR',
'passSlip' => 'Pass Slip',
'vacant' => 'Vacant',
_ => type,
};
pw.TableRow _headerRow(pw.Font bold, List<String> labels) => pw.TableRow(
decoration: const pw.BoxDecoration(color: PdfColors.grey200),
children: labels
.map((l) => pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text(l, style: pw.TextStyle(font: bold, fontSize: 8)),
))
.toList(),
);
pw.Widget _cell(String text, {bool bold = false, pw.Font? boldFont}) =>
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text(
text,
style: bold && boldFont != null
? pw.TextStyle(font: boldFont, fontSize: 8)
: const pw.TextStyle(fontSize: 8),
),
);
pw.TableRow _summaryRow(
String label,
Duration dur, {
bool isBold = false,
pw.Font? boldFont,
}) =>
pw.TableRow(children: [
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text(
label,
style: isBold && boldFont != null
? pw.TextStyle(font: boldFont, fontSize: 9)
: const pw.TextStyle(fontSize: 9),
),
),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text(
_fmt(dur),
style: isBold && boldFont != null
? pw.TextStyle(font: boldFont, fontSize: 9)
: const pw.TextStyle(fontSize: 9),
),
),
]);
// ─── Daily PDF ────────────────────────────────────────────────────────────────
Future<Uint8List> buildDailyWorkLogPdfBytes({
required String personName,
required DateTime date,
required List<WorkLogEntryDto> entries,
required WorkLogSummaryDto summary,
}) async {
final regular = await _font('assets/fonts/Roboto-Regular.ttf');
final bold = await _font('assets/fonts/Roboto-Bold.ttf');
final dateFmt = DateFormat('EEEE, MMMM d, yyyy');
final doc = pw.Document();
doc.addPage(pw.MultiPage(
pageFormat: PdfPageFormat.a4,
margin: const pw.EdgeInsets.all(32),
theme: pw.ThemeData.withFont(base: regular, bold: bold),
header: (ctx) => pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text('Work Log',
style: pw.TextStyle(font: bold, fontSize: 18)),
pw.SizedBox(height: 2),
pw.Text(personName, style: const pw.TextStyle(fontSize: 12)),
pw.Text(dateFmt.format(date),
style:
const pw.TextStyle(fontSize: 11, color: PdfColors.grey700)),
pw.Divider(thickness: 0.5),
],
),
footer: (ctx) => pw.Align(
alignment: pw.Alignment.centerRight,
child: pw.Text(
'Page ${ctx.pageNumber} of ${ctx.pagesCount} • Generated by Tasq',
style:
const pw.TextStyle(fontSize: 8, color: PdfColors.grey600),
),
),
build: (ctx) => [
pw.Table(
border: pw.TableBorder.all(width: 0.4, color: PdfColors.grey400),
columnWidths: const {
0: pw.FixedColumnWidth(68),
1: pw.FixedColumnWidth(68),
2: pw.FixedColumnWidth(58),
3: pw.FlexColumnWidth(),
4: pw.FixedColumnWidth(44),
},
children: [
_headerRow(bold, ['Start', 'End', 'Type', 'Description', 'Duration']),
for (final e in entries.where((e) => !e.isCreatorEvent))
pw.TableRow(children: [
_cell(_fmtTime(e.start)),
_cell(e.end != null ? _fmtTime(e.end!) : ''),
_cell(_typeLabel(e.type)),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(e.title,
style: const pw.TextStyle(fontSize: 8)),
if (e.subtitle != null)
pw.Text(e.subtitle!,
style: const pw.TextStyle(
fontSize: 7, color: PdfColors.grey700)),
],
),
),
_cell(_fmt(e.duration)),
]),
],
),
pw.SizedBox(height: 12),
pw.Text('Summary',
style: pw.TextStyle(font: bold, fontSize: 11)),
pw.SizedBox(height: 4),
pw.Table(
border: pw.TableBorder.all(width: 0.4, color: PdfColors.grey400),
columnWidths: const {
0: pw.FlexColumnWidth(),
1: pw.FixedColumnWidth(60),
},
children: [
if (summary.totalAttendance > Duration.zero)
_summaryRow('Attendance', summary.totalAttendance, boldFont: bold),
if (summary.totalTasks > Duration.zero)
_summaryRow('Tasks', summary.totalTasks, boldFont: bold),
if (summary.totalTickets > Duration.zero)
_summaryRow('Tickets', summary.totalTickets, boldFont: bold),
if (summary.totalIsrs > Duration.zero)
_summaryRow('IT Service Requests', summary.totalIsrs, boldFont: bold),
if (summary.totalPassSlip > Duration.zero)
_summaryRow('Pass Slips', summary.totalPassSlip, boldFont: bold),
_summaryRow('Total Active', summary.totalActive,
isBold: true, boldFont: bold),
],
),
],
));
return doc.save();
}
// ─── Multi-day Summary PDF ────────────────────────────────────────────────────
Future<Uint8List> buildMultiDayWorkLogPdfBytes({
required String personName,
required DateTime rangeStart,
required DateTime rangeEnd,
required String modeLabel,
required List<DailyWorkLogDto> days,
}) async {
final regular = await _font('assets/fonts/Roboto-Regular.ttf');
final bold = await _font('assets/fonts/Roboto-Bold.ttf');
final dateFmt = DateFormat('MMM d, yyyy');
final dayFmt = DateFormat('EEE, MMM d');
final doc = pw.Document();
doc.addPage(pw.MultiPage(
pageFormat: PdfPageFormat.a4,
margin: const pw.EdgeInsets.all(32),
theme: pw.ThemeData.withFont(base: regular, bold: bold),
header: (ctx) => pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text('Work Log — $modeLabel',
style: pw.TextStyle(font: bold, fontSize: 18)),
pw.SizedBox(height: 2),
pw.Text(personName, style: const pw.TextStyle(fontSize: 12)),
pw.Text(
'${dateFmt.format(rangeStart)} '
'${dateFmt.format(rangeEnd.subtract(const Duration(days: 1)))}',
style:
const pw.TextStyle(fontSize: 11, color: PdfColors.grey700),
),
pw.Divider(thickness: 0.5),
],
),
footer: (ctx) => pw.Align(
alignment: pw.Alignment.centerRight,
child: pw.Text(
'Page ${ctx.pageNumber} of ${ctx.pagesCount} • Generated by Tasq',
style:
const pw.TextStyle(fontSize: 8, color: PdfColors.grey600),
),
),
build: (ctx) => [
pw.Table(
border: pw.TableBorder.all(width: 0.4, color: PdfColors.grey400),
columnWidths: const {
0: pw.FixedColumnWidth(80),
1: pw.FixedColumnWidth(54),
2: pw.FixedColumnWidth(40),
3: pw.FixedColumnWidth(40),
4: pw.FixedColumnWidth(36),
5: pw.FixedColumnWidth(46),
6: pw.FixedColumnWidth(38),
7: pw.FixedColumnWidth(48),
},
children: [
_headerRow(bold,
['Date', 'Attend.', 'Tasks', 'Tickets', 'ISR', 'Pass Slip', 'Idle', 'Total']),
for (final day in days)
pw.TableRow(
decoration: day.hasData
? null
: const pw.BoxDecoration(color: PdfColors.grey100),
children: [
_cell(dayFmt.format(day.date)),
_cell(day.hasData ? _fmt(day.summary.totalAttendance) : ''),
_cell(day.hasData ? _fmt(day.summary.totalTasks) : ''),
_cell(day.hasData ? _fmt(day.summary.totalTickets) : ''),
_cell(day.hasData ? _fmt(day.summary.totalIsrs) : ''),
_cell(day.hasData ? _fmt(day.summary.totalPassSlip) : ''),
_cell(day.hasData ? _fmt(day.summary.totalVacant) : ''),
_cell(day.hasData ? _fmt(day.summary.totalActive) : ''),
],
),
],
),
],
));
return doc.save();
}