Modul:ProjectTasks

Aus Kyffhäuser KI
Zur Navigation springen Zur Suche springen

Die Dokumentation für dieses Modul kann unter Modul:ProjectTasks/Doku erstellt werden

local p = {}

-- ---------- helpers ----------
local function norm(s)
  if not s then return "" end
  s = mw.text.trim(tostring(s))
  return mw.ustring.lower(s)
end

local function stripNs(title)
  return mw.ustring.gsub(title or "", "^[^:]+:", "")
end

local function wikiLink(title, label)
  if not title or title == "" then return "" end
  label = label or stripNs(title)
  return string.format("[[%s|%s]]", title, label)
end

local function getPrintouts(row)
  if row.printouts then return row.printouts end
  return row
end

local function asList(v)
  if not v then return {} end
  if type(v) ~= "table" then return { v } end
  return v
end

local function valueToString(v)
  if type(v) == "table" then
    if v.fulltext then return v.fulltext end
    if v.value then return tostring(v.value) end
    if v[1] then return valueToString(v[1]) end
  end
  if type(v) == "string" then return v end
  if type(v) == "number" then return tostring(v) end
  return nil
end

local function firstText(po, key)
  local v = po[key]
  if not v then return nil end
  if type(v) == "table" and v[1] ~= nil then
    return valueToString(v[1])
  end
  return valueToString(v)
end

local function listPages(po, key)
  local out = {}
  local v = po[key]
  for _, it in ipairs(asList(v)) do
    local t = valueToString(it)
    if t and t ~= "" then table.insert(out, t) end
  end
  return out
end

-- IMPORTANT: robust title extraction from SMW results
local function rowTitle(row)
  -- SemanticScribunto variants:
  if row.fulltext then return row.fulltext end
  if row.title then return row.title end
  if row.page and type(row.page) == "table" and row.page.fulltext then return row.page.fulltext end
  if row[1] then
    if type(row[1]) == "string" then return row[1] end
    if type(row[1]) == "table" and row[1].fulltext then return row[1].fulltext end
  end
  -- Sometimes label key is the page itself if mainlabel was default
  if row[""] and type(row[""]) == "table" and row[""].fulltext then return row[""].fulltext end
  return nil
end

-- ---------- load all tasks for a project ----------
local function loadProjectTasks(project)
  local q = {
    string.format("[[Gehört zu Projekt::%s]]", project),
    "?Status",
    "?Teil von",
    "?Abhängig von",
    "limit=2000"
  }
  local res = mw.smw.ask(q) or {}

  local tasks = {}

  for _, row in ipairs(res) do
    local title = rowTitle(row)
    if title then
      local po = getPrintouts(row)
      local st = norm(firstText(po, "Status"))
      local parent = firstText(po, "Teil von")
      local deps = listPages(po, "Abhängig von")
      if st == "" then st = "todo" end

      tasks[title] = tasks[title] or { deps = {}, blockers = {} }
      tasks[title].status = st
      tasks[title].parent = parent
      tasks[title].deps = deps
    end
  end

  return tasks
end

local function computeBlockedBy(tasks, title)
  local data = tasks[title]
  if not data then return {} end
  local blockedBy = {}

  for _, dep in ipairs(data.deps or {}) do
    local depData = tasks[dep]
    local depStatus = depData and depData.status or "todo"
    if norm(depStatus) ~= "done" then
      table.insert(blockedBy, dep)
    end
  end

  table.sort(blockedBy)
  return blockedBy
end

-- ========== PUBLIC: Ready/Blocked lists ==========
function p.readyBlocked(frame)
  local args = frame.args
  local parentArgs = frame:getParent() and frame:getParent().args or {}
  local project = args.project or parentArgs.project
  if not project or project == "" then
    return "Fehler: project=... fehlt."
  end

  local tasks = loadProjectTasks(project)

  local ready, blocked, onhold, done = {}, {}, {}, {}

  for title, data in pairs(tasks) do
    local st = norm(data.status)
    local bb = computeBlockedBy(tasks, title)

    if st == "done" then
      table.insert(done, title)
    elseif st == "onhold" then
      table.insert(onhold, title)
    else
      if #bb == 0 then table.insert(ready, title) else table.insert(blocked, title) end
    end
  end

  table.sort(ready); table.sort(blocked); table.sort(onhold); table.sort(done)

  local function renderList(list, showBlockedBy)
    if #list == 0 then return "''(keine)''" end
    local out = {}
    table.insert(out, '{| class="wikitable sortable" style="width:100%;"')
    table.insert(out, "! Task")
    if showBlockedBy then table.insert(out, "! Blockiert durch") end
    table.insert(out, "! Status")
    for _, t in ipairs(list) do
      local st = tasks[t] and tasks[t].status or "todo"
      table.insert(out, "|-")
      table.insert(out, "| " .. wikiLink(t))
      if showBlockedBy then
        local bb = computeBlockedBy(tasks, t)
        local bbLinks = {}
        for _, dep in ipairs(bb) do table.insert(bbLinks, wikiLink(dep)) end
        table.insert(out, "| " .. (#bbLinks > 0 and table.concat(bbLinks, ", ") or "–"))
      end
      table.insert(out, "| " .. st)
    end
    table.insert(out, "|}")
    return table.concat(out, "\n")
  end

  local out = {}
  table.insert(out, "== Ready ==")
  table.insert(out, "Tasks, die startbar sind (alle Dependencies erledigt oder keine).")
  table.insert(out, renderList(ready, false))

  table.insert(out, "\n== Blocked ==")
  table.insert(out, "Tasks, die blockiert sind (mindestens eine Dependency ist nicht ''done'').")
  table.insert(out, renderList(blocked, true))

  table.insert(out, "\n== On hold ==")
  table.insert(out, renderList(onhold, false))

  table.insert(out, "\n== Done ==")
  table.insert(out, renderList(done, false))

  return table.concat(out, "\n")
end

-- ========== PUBLIC: Tree view ==========
function p.tree(frame)
  local args = frame.args
  local parentArgs = frame:getParent() and frame:getParent().args or {}
  local project = args.project or parentArgs.project
  local root = args.root or parentArgs.root

  if not project or project == "" then
    return "Fehler: project=... fehlt."
  end

  local tasks = loadProjectTasks(project)

  local children = {}
  for title, data in pairs(tasks) do
    local parent = data.parent
    if parent and parent ~= "" then
      children[parent] = children[parent] or {}
      table.insert(children[parent], title)
    end
  end
  for _, list in pairs(children) do table.sort(list) end

  local roots = {}
  if root and root ~= "" then
    roots = { root }
  else
    for title, data in pairs(tasks) do
      local parent = data.parent
      if not parent or parent == "" or not tasks[parent] then
        table.insert(roots, title)
      end
    end
    table.sort(roots)
  end

  local function badgeFor(t)
    local st = tasks[t] and tasks[t].status or "todo"
    local bb = computeBlockedBy(tasks, t)
    if norm(st) == "done" then return " ✅" end
    if norm(st) == "onhold" then return " ⏸️" end
    if #bb > 0 then return " ⛔" end
    if norm(st) == "doing" then return " ▶️" end
    return ""
  end

  local function renderNode(t, depth)
    depth = depth or 0
    local st = tasks[t] and tasks[t].status or "todo"
    local line = string.rep("*", math.min(depth + 1, 6)) ..
      " " .. wikiLink(t) .. badgeFor(t) ..
      " <span style='opacity:.7'>(" .. st .. ")</span>"

    local out = { line }
    for _, c in ipairs(children[t] or {}) do
      local sub = renderNode(c, depth + 1)
      for _, l in ipairs(sub) do table.insert(out, l) end
    end
    return out
  end

  local out = {}
  table.insert(out, "== Task Tree ==")
  for _, r in ipairs(roots) do
    for _, l in ipairs(renderNode(r, 0)) do table.insert(out, l) end
  end
  return table.concat(out, "\n")
end

return p