160 lines
6.1 KiB
C++
160 lines
6.1 KiB
C++
// panel_board.cpp — columns + cards Kanban panel.
|
|
#include "panels.h"
|
|
#include "core/icons_tabler.h"
|
|
|
|
#include <imgui.h>
|
|
#include <ctime>
|
|
#include <thread>
|
|
#include <mutex>
|
|
|
|
namespace kanban_cpp {
|
|
|
|
void refresh_data(AppState& s) {
|
|
std::string err;
|
|
auto cards = list_cards(s.cfg, err);
|
|
std::string err_cards = err; err.clear();
|
|
auto columns = list_columns(s.cfg, err);
|
|
std::string err_cols = err;
|
|
bool ok = health(s.cfg);
|
|
int64_t ts = std::time(nullptr);
|
|
std::lock_guard<std::mutex> lock(s.mu);
|
|
s.cards = std::move(cards);
|
|
s.columns = std::move(columns);
|
|
s.last_refresh_error.clear();
|
|
if (!err_cards.empty()) s.last_refresh_error = "cards: " + err_cards;
|
|
if (!err_cols.empty()) s.last_refresh_error += " columns: " + err_cols;
|
|
s.backend_ok = ok;
|
|
s.last_refresh_ts = ts;
|
|
}
|
|
|
|
void draw_board(AppState& s, bool* p_open) {
|
|
if (!ImGui::Begin(TI_LAYOUT_KANBAN " Board", p_open)) {
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
// Snapshot bajo lock — refresh corre en thread separado.
|
|
std::vector<Card> cards_snap;
|
|
std::vector<Column> cols_snap;
|
|
bool backend_ok_snap;
|
|
std::string err_snap;
|
|
std::string sse_snap;
|
|
{
|
|
std::lock_guard<std::mutex> lock(s.mu);
|
|
cards_snap = s.cards;
|
|
cols_snap = s.columns;
|
|
backend_ok_snap = s.backend_ok;
|
|
err_snap = s.last_refresh_error;
|
|
sse_snap = s.sse_status;
|
|
}
|
|
|
|
// Toolbar — refresh corre en thread separado (no bloquea frame).
|
|
if (ImGui::Button(TI_REFRESH " Refresh")) {
|
|
std::thread([&s](){ refresh_data(s); }).detach();
|
|
}
|
|
ImGui::SameLine();
|
|
if (backend_ok_snap) {
|
|
ImGui::TextColored(ImVec4(0.4f, 0.85f, 0.4f, 1.0f), TI_CHECK " backend :8403");
|
|
} else {
|
|
ImGui::TextColored(ImVec4(0.85f, 0.4f, 0.4f, 1.0f), TI_ALERT_TRIANGLE " backend offline (:8403)");
|
|
}
|
|
|
|
// SSE live badge — refleja el estado del stream push del backend.
|
|
ImGui::SameLine();
|
|
if (sse_snap == "connected") {
|
|
ImGui::TextColored(ImVec4(0.4f, 0.85f, 0.4f, 1.0f), TI_BROADCAST " live");
|
|
} else if (sse_snap == "connecting") {
|
|
ImGui::TextColored(ImVec4(0.85f, 0.85f, 0.4f, 1.0f), TI_LOADER " connecting");
|
|
} else if (sse_snap == "disconnected") {
|
|
ImGui::TextColored(ImVec4(0.85f, 0.4f, 0.4f, 1.0f), TI_PLUG_CONNECTED_X " disconnected");
|
|
} else {
|
|
// "error: <msg>" o cualquier otro string
|
|
ImGui::TextColored(ImVec4(0.85f, 0.4f, 0.4f, 1.0f), TI_PLUG_CONNECTED_X " %s", sse_snap.c_str());
|
|
}
|
|
|
|
if (!err_snap.empty()) {
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(0.85f, 0.6f, 0.2f, 1.0f), "%s", err_snap.c_str());
|
|
}
|
|
ImGui::Separator();
|
|
|
|
// Empty state
|
|
if (cols_snap.empty()) {
|
|
ImGui::TextDisabled("No columns yet. Pulsa Refresh o lanza el backend en :8403.");
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
// Render columns left-to-right
|
|
const float col_w = 280.0f;
|
|
if (ImGui::BeginChild("##board_scroll", ImVec2(0, 0), false,
|
|
ImGuiWindowFlags_HorizontalScrollbar)) {
|
|
for (size_t ci = 0; ci < cols_snap.size(); ++ci) {
|
|
const auto& col = cols_snap[ci];
|
|
ImGui::SameLine();
|
|
ImGui::BeginChild((std::string("##col_") + col.id).c_str(),
|
|
ImVec2(col_w, 0), true);
|
|
ImGui::TextUnformatted(col.name.c_str());
|
|
ImGui::SameLine();
|
|
int count = 0;
|
|
for (const auto& c : cards_snap) if (c.column_id == col.id) ++count;
|
|
ImGui::TextDisabled("(%d)", count);
|
|
ImGui::Separator();
|
|
|
|
for (const auto& card : cards_snap) {
|
|
if (card.column_id != col.id) continue;
|
|
ImGui::PushID(card.id.c_str());
|
|
ImGui::BeginChild("##card", ImVec2(0, 70), true,
|
|
ImGuiWindowFlags_NoScrollbar);
|
|
ImGui::TextUnformatted(card.title.c_str());
|
|
if (!card.priority.empty()) {
|
|
ImVec4 col_p(0.6f, 0.6f, 0.6f, 1);
|
|
if (card.priority == "high") col_p = {0.95f, 0.55f, 0.2f, 1};
|
|
else if (card.priority == "critical") col_p = {0.95f, 0.25f, 0.25f, 1};
|
|
else if (card.priority == "low") col_p = {0.45f, 0.7f, 0.95f, 1};
|
|
ImGui::TextColored(col_p, TI_FLAG " %s", card.priority.c_str());
|
|
}
|
|
if (!card.assignee.empty()) {
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled(TI_USER " %s", card.assignee.c_str());
|
|
}
|
|
ImGui::EndChild();
|
|
if (ImGui::IsItemHovered() && ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
|
ImGui::OpenPopup("##card_ctx");
|
|
}
|
|
if (ImGui::BeginPopup("##card_ctx")) {
|
|
ImGui::TextDisabled("Move to:");
|
|
for (const auto& tgt : cols_snap) {
|
|
if (tgt.id == card.column_id) continue;
|
|
if (ImGui::MenuItem(tgt.name.c_str())) {
|
|
std::thread([&s, card_id=card.id, tgt_id=tgt.id](){
|
|
std::string err;
|
|
if (!move_card(s.cfg, card_id, tgt_id, err)) {
|
|
std::lock_guard<std::mutex> lock(s.mu);
|
|
s.last_refresh_error = "move: " + err;
|
|
return;
|
|
}
|
|
refresh_data(s);
|
|
}).detach();
|
|
}
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem(TI_PLAYER_PLAY " Launch agent workflow")) {
|
|
std::string run_id, err;
|
|
if (!launch_workflow(s.cfg, card.id, run_id, err))
|
|
s.last_refresh_error = "launch: " + err;
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::EndChild();
|
|
}
|
|
}
|
|
ImGui::EndChild();
|
|
|
|
ImGui::End();
|
|
}
|
|
|
|
} // namespace kanban_cpp
|