sara-tasks 2.7.9

Sara — folder-aware task manager
use anyhow::Result;
use rusqlite::Connection;

use crate::domain::Status;
use crate::infrastructure::db;

use crate::domain::Task;

use super::types::{BoardState, CardInfo, Freshness, IssueNode};

pub(super) fn build_state(
    conn: &Connection,
    project: String,
    show_finished: bool,
    prev: Option<&BoardState>,
) -> Result<BoardState> {
    let all = db::list_tasks_for_board(conn, &project)?;
    let (groups, standalone_all) = db::group_tasks_by_issue(conn, &all)?;
    let titles = db::github_issue_titles_for_project(conn, &project)?;
    let imported = db::github_synced_task_uuids(conn)?;
    let badges = db::link_flags_by_task(conn).unwrap_or_default();
    let head = db::get_project(conn, &project)
        .ok()
        .flatten()
        .and_then(|p| p.path)
        .and_then(|p| crate::infrastructure::git::head_commit(std::path::Path::new(&p)));
    let deps = db::dep_info_by_task(conn).unwrap_or_default();
    let cards = all
        .iter()
        .filter(|t| show_finished || t.status != Status::Completed)
        .map(|t| {
            let mut card = card_info(conn, t, head.as_deref());
            card.blocked_by = deps
                .get(&t.uuid.to_string())
                .map(|d| d.blocked_by.clone())
                .unwrap_or_default();
            (t.uuid.to_string(), card)
        })
        .collect();

    let issues: Vec<IssueNode> = groups
        .into_iter()
        .filter_map(|g| {
            let total = g.tasks.len();
            let done = g
                .tasks
                .iter()
                .filter(|t| t.status == Status::Completed)
                .count();
            let title = g
                .tasks
                .iter()
                .find_map(|t| titles.get(&t.uuid.to_string()).cloned());
            let tasks: Vec<_> = if show_finished {
                g.tasks
            } else {
                g.tasks
                    .into_iter()
                    .filter(|t| t.status != Status::Completed)
                    .collect()
            };
            if tasks.is_empty() {
                return None;
            }
            let expanded = prev
                .and_then(|p| {
                    p.issues
                        .iter()
                        .find(|i| i.owner_repo == g.owner_repo && i.number == g.number)
                })
                .map(|i| i.expanded)
                .unwrap_or(false);
            Some(IssueNode {
                owner_repo: g.owner_repo,
                number: g.number,
                title,
                tasks,
                done,
                total,
                expanded,
            })
        })
        .collect();

    let standalone: Vec<_> = if show_finished {
        standalone_all
    } else {
        standalone_all
            .into_iter()
            .filter(|t| t.status != Status::Completed)
            .collect()
    };

    let pending = all.iter().filter(|t| t.status == Status::Pending).count();
    let done = all.len() - pending;

    Ok(BoardState {
        project,
        issues,
        standalone,
        badges,
        cards,
        filter: prev.map(|p| p.filter.clone()).unwrap_or_default(),
        filtering: false,
        preview: prev.is_some_and(|p| p.preview),
        show_finished,
        imported,
        selected: 0,
        scroll: 0,
        pending,
        done,
    })
}

pub(super) fn card_info(conn: &Connection, task: &Task, head: Option<&str>) -> CardInfo {
    let validated = db::get_guide_fields(conn, &task.uuid)
        .ok()
        .and_then(|g| g.validated_commit);
    let freshness = match (validated, head) {
        (None, _) => Freshness::Unvalidated,
        (Some(v), Some(h)) if v != h => Freshness::Stale,
        (Some(_), _) => Freshness::Valid,
    };
    let checklist = db::get_checklist(conn, &task.uuid).unwrap_or_default();
    let accept: Vec<_> = checklist
        .iter()
        .filter(|c| c.kind == db::STEP_KIND_ACCEPTANCE)
        .collect();
    let step = checklist
        .iter()
        .find(|c| c.kind == db::STEP_KIND_STEP && !c.done)
        .map(|c| c.text.clone());
    let feedback = db::get_open_feedback(conn, &task.uuid).unwrap_or_default();
    CardInfo {
        freshness,
        branch: db::get_task_branch(conn, &task.uuid).map(|b| b.branch),
        accept_done: accept.iter().filter(|c| c.done).count(),
        accept_total: accept.len(),
        step,
        feedback: feedback.len(),
        revise: feedback.iter().filter(|a| a.request_revision).count(),
        blocked_by: vec![],
    }
}

#[cfg(test)]
#[path = "../../../tests/unit/commands/board/state.rs"]
mod tests;