import AppKit // MARK: - Work Log View Builder enum WorkLogViewBuilder { static func build(in window: NSWindow, isPrimary: Bool, delegate: AppDelegate) { let size = window.frame.size let view = NSView(frame: NSRect(origin: .zero, size: size)) view.wantsLayer = true let bgColor = Settings.shared.gradientColor.withAlphaComponent(0.92) view.layer?.backgroundColor = bgColor.cgColor let entries = WorkLogStore.shared.todayEntries() let summary = WorkLogStore.shared.todaySummary() // Title let title = NSTextField(labelWithString: "Today's Work Log") title.font = NSFont.systemFont(ofSize: 48, weight: .bold) title.textColor = .white title.alignment = .center title.sizeToFit() title.frame.origin = CGPoint( x: (size.width - title.frame.width) / 2, y: size.height - 120 ) view.addSubview(title) // Summary stats let hours = summary.totalSeconds / 3600 let mins = (summary.totalSeconds % 3600) / 60 let timeStr = hours > 0 ? String(format: "%dh %dm", hours, mins) : String(format: "%dm", mins) let completedCount = entries.filter { $0.completed }.count let summaryText = "\(summary.intervals) intervals | \(completedCount) completed | \(timeStr) focused" let summaryLabel = NSTextField(labelWithString: summaryText) summaryLabel.font = NSFont.systemFont(ofSize: 22, weight: .medium) summaryLabel.textColor = NSColor.white.withAlphaComponent(0.85) summaryLabel.alignment = .center summaryLabel.sizeToFit() summaryLabel.frame.origin = CGPoint( x: (size.width - summaryLabel.frame.width) / 2, y: size.height - 165 ) view.addSubview(summaryLabel) if isPrimary { // Close button let closeBtn = NSButton(frame: NSRect(x: size.width - 140, y: size.height - 60, width: 120, height: 36)) closeBtn.title = "Close" closeBtn.bezelStyle = .rounded closeBtn.font = NSFont.systemFont(ofSize: 14, weight: .medium) closeBtn.target = delegate closeBtn.action = #selector(AppDelegate.dismissWorkLogAndResume) view.addSubview(closeBtn) // Scrollable entry list let scrollW: CGFloat = min(700, size.width - 100) let scrollH: CGFloat = size.height - 300 let scrollX = (size.width - scrollW) / 2 let scrollY: CGFloat = 120 let scrollView = NSScrollView(frame: NSRect(x: scrollX, y: scrollY, width: scrollW, height: scrollH)) scrollView.hasVerticalScroller = true scrollView.drawsBackground = false scrollView.autohidesScrollers = true let contentView = buildEntryList(entries: entries, width: scrollW) scrollView.documentView = contentView view.addSubview(scrollView) // Bottom buttons: Back to Work | Quit let btnW: CGFloat = 180 let btnH: CGFloat = 44 let gap: CGFloat = 24 let totalW = btnW * 3 + gap * 2 let startX = (size.width - totalW) / 2 let btnY: CGFloat = 50 let copyBtn = NSButton(frame: NSRect(x: startX, y: btnY, width: btnW, height: btnH)) copyBtn.title = "Copy" copyBtn.bezelStyle = .rounded copyBtn.font = NSFont.systemFont(ofSize: 16, weight: .semibold) copyBtn.target = delegate copyBtn.action = #selector(AppDelegate.copyWorkLog) view.addSubview(copyBtn) let resumeBtn = NSButton(frame: NSRect(x: startX + btnW + gap, y: btnY, width: btnW, height: btnH)) resumeBtn.title = "Back to Work" resumeBtn.bezelStyle = .rounded resumeBtn.font = NSFont.systemFont(ofSize: 16, weight: .semibold) resumeBtn.target = delegate resumeBtn.action = #selector(AppDelegate.dismissWorkLogAndResume) view.addSubview(resumeBtn) let quitBtn = NSButton(frame: NSRect(x: startX + (btnW + gap) * 2, y: btnY, width: btnW, height: btnH)) quitBtn.title = "Quit Pommedoro" quitBtn.bezelStyle = .rounded quitBtn.font = NSFont.systemFont(ofSize: 16, weight: .semibold) quitBtn.target = delegate quitBtn.action = #selector(AppDelegate.closePommedoro) view.addSubview(quitBtn) } window.contentView = view } // MARK: - Entry List private static func buildEntryList(entries: [WorkLogEntry], width: CGFloat) -> NSView { let rowHeight: CGFloat = 86 let padding: CGFloat = 12 let totalHeight = max(CGFloat(entries.count) * (rowHeight + padding) + padding, 100) let container = NSView(frame: NSRect(x: 0, y: 0, width: width, height: totalHeight)) if entries.isEmpty { let empty = NSTextField(labelWithString: "No intervals logged yet today.\nStart a work cycle and your log will appear here.") empty.font = NSFont.systemFont(ofSize: 18, weight: .medium) empty.textColor = NSColor.white.withAlphaComponent(0.7) empty.alignment = .center empty.maximumNumberOfLines = 0 empty.preferredMaxLayoutWidth = width - 40 empty.sizeToFit() empty.frame.origin = CGPoint( x: (width - empty.frame.width) / 2, y: (totalHeight - empty.frame.height) / 2 ) container.addSubview(empty) return container } let formatter = DateFormatter() formatter.dateFormat = "h:mm a" for (index, entry) in entries.enumerated() { let y = totalHeight - CGFloat(index + 1) * (rowHeight + padding) let row = NSView(frame: NSRect(x: 0, y: y, width: width, height: rowHeight)) row.wantsLayer = true row.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.12).cgColor row.layer?.cornerRadius = 8 // Time and duration let timeStr = formatter.string(from: entry.date) let durMins = entry.durationSeconds / 60 let status = entry.completed ? "completed" : "partial" let header = "\(timeStr) — \(durMins)m (\(status))" let headerLabel = NSTextField(labelWithString: header) headerLabel.font = NSFont.monospacedSystemFont(ofSize: 13, weight: .semibold) headerLabel.textColor = NSColor.white.withAlphaComponent(0.9) headerLabel.frame = NSRect(x: 16, y: rowHeight - 24, width: width - 32, height: 18) row.addSubview(headerLabel) // Intent (always show line; use "—" when nil or empty) let rawIntent = entry.intent?.trimmingCharacters(in: .whitespacesAndNewlines) let intentText = (rawIntent?.isEmpty != false) ? "—" : (rawIntent ?? "—") let intentLabel = NSTextField(labelWithString: "Intent: \(intentText)") intentLabel.font = NSFont.systemFont(ofSize: 12, weight: .regular) intentLabel.textColor = NSColor.white.withAlphaComponent(0.75) intentLabel.frame = NSRect(x: 16, y: rowHeight - 44, width: width - 32, height: 16) intentLabel.lineBreakMode = .byTruncatingTail if let intent = rawIntent, !intent.isEmpty { intentLabel.toolTip = intent } row.addSubview(intentLabel) // Reflection let reflText = entry.reflection ?? "—" let reflLabel = NSTextField(labelWithString: "Reflection: \(reflText)") reflLabel.font = NSFont.systemFont(ofSize: 12, weight: .regular) reflLabel.textColor = NSColor.white.withAlphaComponent(0.75) reflLabel.frame = NSRect(x: 16, y: rowHeight - 62, width: width - 32, height: 16) reflLabel.lineBreakMode = .byTruncatingTail row.addSubview(reflLabel) // Suggestion feedback let suggText = formatSuggestionFeedback(entry) let suggLabel = NSTextField(labelWithString: suggText) suggLabel.font = NSFont.systemFont(ofSize: 12, weight: .regular) suggLabel.textColor = NSColor.white.withAlphaComponent(0.65) suggLabel.frame = NSRect(x: 16, y: rowHeight - 80, width: width - 32, height: 16) suggLabel.lineBreakMode = .byTruncatingTail row.addSubview(suggLabel) container.addSubview(row) } return container } private static func formatSuggestionFeedback(_ entry: WorkLogEntry) -> String { guard let suggestion = entry.suggestion, !suggestion.isEmpty else { return "Wellness: —" } let fbStr: String switch entry.suggestionFeedback { case .didIt: let wellStr: String switch entry.wellnessReflection { case .great: wellStr = " — felt great" case .same: wellStr = " — about the same" case .notReally: wellStr = " — not really" case .none: wellStr = "" } fbStr = "did it\(wellStr)" case .skipped: fbStr = "skipped" case .dismissed: fbStr = "removed from pool" case .none: fbStr = "—" } return "Wellness: \(suggestion) (\(fbStr))" } }