Files
apex-public/internal/tui/reports_views.go
T
2026-07-06 11:05:50 -04:00

178 lines
4.2 KiB
Go

package tui
import (
"database/sql"
"fmt"
"os"
"os/exec"
"github.com/charmbracelet/bubbles/table"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type ReportsModel struct {
db *sql.DB
table table.Model
rows []reportRow
status string
width int
height int
}
type reportRow struct {
ID int
Seq int
Company string
Title string
Score float64
Legitimacy string
FilePath string
CreatedAt string
}
func NewReportsModel(db *sql.DB) *ReportsModel {
m := &ReportsModel{db: db, status: "[r] refresh [enter] open report"}
m.load()
m.table = newReportsTable(m.rows, 0, 0)
return m
}
func (m *ReportsModel) load() {
m.rows = nil
if m.db == nil {
m.status = "no database"
return
}
rows, err := m.db.Query(`
SELECT r.id, COALESCE(r.seq,0), COALESCE(j.company,''), COALESCE(j.title,''),
COALESCE(r.score, 0), COALESCE(r.legitimacy, ''), COALESCE(r.file_path, ''),
COALESCE(r.created_at, '')
FROM reports r
LEFT JOIN applications a ON a.id = r.application_id
LEFT JOIN jobs j ON j.id = a.job_id
ORDER BY r.id DESC
LIMIT 200
`)
if err != nil {
m.status = fmt.Sprintf("query failed: %v", err)
return
}
defer rows.Close()
for rows.Next() {
var r reportRow
if err := rows.Scan(&r.ID, &r.Seq, &r.Company, &r.Title, &r.Score, &r.Legitimacy, &r.FilePath, &r.CreatedAt); err != nil {
continue
}
m.rows = append(m.rows, r)
}
}
func reportsColumns() []table.Column {
return []table.Column{
{Title: "#", Width: 5},
{Title: "Company", Width: 22},
{Title: "Title", Width: 30},
{Title: "Score", Width: 6},
{Title: "Legit", Width: 10},
{Title: "Created", Width: 19},
}
}
func newReportsTable(rows []reportRow, w, h int) table.Model {
cols := flexColumns(w, reportsColumns(), []int{1, 2})
trows := make([]table.Row, 0, len(rows))
for _, r := range rows {
trows = append(trows, reportRowToTable(r))
}
t := table.New(
table.WithColumns(cols),
table.WithRows(trows),
table.WithFocused(true),
table.WithHeight(tableHeight(h)),
)
s := table.DefaultStyles()
s.Header = s.Header.BorderStyle(lipgloss.NormalBorder()).
BorderForeground(ColorBorder).BorderBottom(true).Bold(true).Foreground(ColorAccentAlt)
s.Selected = s.Selected.Bold(true).Foreground(ColorText).Background(ColorHighlight)
t.SetStyles(s)
return t
}
func reportRowToTable(r reportRow) table.Row {
seq := "-"
if r.Seq > 0 {
seq = fmt.Sprintf("#%03d", r.Seq)
}
score := "-"
if r.Score > 0 {
score = fmt.Sprintf("%.1f", r.Score)
}
return table.Row{seq, r.Company, r.Title, score, r.Legitimacy, r.CreatedAt}
}
func (m *ReportsModel) Init() tea.Cmd { return nil }
func (m *ReportsModel) Resize(w, h int) {
m.width = w
m.height = h
m.table.SetHeight(tableHeight(h))
m.table.SetWidth(w - 2)
m.table.SetColumns(flexColumns(w-2, reportsColumns(), []int{1, 2}))
}
func (m *ReportsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "r":
m.load()
m.refreshTable()
m.status = fmt.Sprintf("%d reports", len(m.rows))
case "enter":
if cur := m.table.Cursor(); cur < len(m.rows) {
path := m.rows[cur].FilePath
if path == "" {
m.status = "no file_path on this report"
return m, nil
}
if _, err := os.Stat(path); err != nil {
m.status = fmt.Sprintf("missing: %s", path)
return m, nil
}
m.openInPager(path)
}
}
}
var cmd tea.Cmd
m.table, cmd = m.table.Update(msg)
return m, cmd
}
func (m *ReportsModel) refreshTable() {
trows := make([]table.Row, 0, len(m.rows))
for _, r := range m.rows {
trows = append(trows, reportRowToTable(r))
}
m.table.SetRows(trows)
}
func (m *ReportsModel) openInPager(path string) {
pager := os.Getenv("PAGER")
if pager == "" {
pager = "less"
}
cmd := exec.Command(pager, path)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
_ = cmd.Run()
}
func (m *ReportsModel) View() string {
header := StyleTitle.Render("Reports — evaluation history")
sub := StyleSubtitle.Render(fmt.Sprintf("%d reports · [r] refresh · [enter] open in $PAGER", len(m.rows)))
status := StyleStatusMuted.Render(m.status)
return lipgloss.JoinVertical(lipgloss.Left, header, sub, "", m.table.View(), "", status)
}