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 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 _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 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 buildDailyWorkLogPdfBytes({ required String personName, required DateTime date, required List 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 buildMultiDayWorkLogPdfBytes({ required String personName, required DateTime rangeStart, required DateTime rangeEnd, required String modeLabel, required List 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(); }