feat: initial scaffold of kanban_cpp v2 (issue 0130)
Frontend C++ ImGui (main.cpp + 4 paneles) + backend Go (HTTP + SQLite + fsnotify + SSE). Reusa parse/scan/watch funcs del registry (issue 0130a).
This commit is contained in:
+150
@@ -0,0 +1,150 @@
|
||||
#include "panels.h"
|
||||
#include "data.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/icons_tabler.h"
|
||||
|
||||
namespace kanban {
|
||||
|
||||
static const char* k_columns[] = {"pendiente", "in-progress", "bloqueado", "completado"};
|
||||
static const int k_n_columns = sizeof(k_columns) / sizeof(k_columns[0]);
|
||||
|
||||
static ImU32 priority_color(const std::string& p) {
|
||||
if (p == "critica") return IM_COL32(255, 80, 80, 255);
|
||||
if (p == "alta") return IM_COL32(255, 165, 0, 255);
|
||||
if (p == "media") return IM_COL32(120, 170, 255, 255);
|
||||
if (p == "baja") return IM_COL32(140, 140, 140, 255);
|
||||
return IM_COL32(180, 180, 180, 255);
|
||||
}
|
||||
|
||||
static void draw_card(const Issue& iss) {
|
||||
ImGui::PushID(iss.id.c_str());
|
||||
|
||||
ImVec2 size(0.0f, 64.0f);
|
||||
ImGui::BeginChild("card", size, true, ImGuiWindowFlags_NoScrollbar);
|
||||
|
||||
// Header: id + priority badge
|
||||
ImGui::TextColored(ImColor(180, 180, 180, 255), "%s", iss.id.c_str());
|
||||
ImGui::SameLine();
|
||||
{
|
||||
ImVec2 cur = ImGui::GetCursorScreenPos();
|
||||
ImU32 c = priority_color(iss.priority);
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
float r = 5.0f;
|
||||
dl->AddCircleFilled(ImVec2(cur.x + r, cur.y + r + 2), r, c);
|
||||
ImGui::Dummy(ImVec2(2 * r + 4, 2 * r));
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted(iss.priority.c_str());
|
||||
}
|
||||
|
||||
// Title (truncate to ~60 chars)
|
||||
std::string t = iss.title;
|
||||
if (t.size() > 70) t = t.substr(0, 67) + "...";
|
||||
ImGui::TextWrapped("%s", t.c_str());
|
||||
|
||||
// First domain chip
|
||||
if (!iss.domain.empty()) {
|
||||
ImGui::TextColored(ImColor(140, 200, 255, 255), TI_TAG " %s", iss.domain[0].c_str());
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
if (ImGui::IsItemClicked()) {
|
||||
refresh_issue_detail(iss.id);
|
||||
}
|
||||
|
||||
// Drag source
|
||||
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) {
|
||||
ImGui::SetDragDropPayload("KANBAN_ISSUE", iss.id.c_str(), iss.id.size() + 1);
|
||||
ImGui::Text("%s %s", iss.id.c_str(), t.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
static void draw_column(const char* status_key, std::vector<const Issue*>& items) {
|
||||
ImGui::TableNextColumn();
|
||||
// Column header with count
|
||||
ImGui::PushFont(nullptr);
|
||||
ImGui::TextColored(ImColor(220, 220, 220, 255), "%s (%zu)", status_key, items.size());
|
||||
ImGui::PopFont();
|
||||
ImGui::Separator();
|
||||
|
||||
// Drop target on the whole column
|
||||
ImVec2 region = ImGui::GetContentRegionAvail();
|
||||
ImGui::BeginChild((std::string("col_") + status_key).c_str(), region, false);
|
||||
|
||||
for (const auto* iss : items) {
|
||||
draw_card(*iss);
|
||||
}
|
||||
// Bottom drop zone
|
||||
ImGui::InvisibleButton("col_drop", ImVec2(-1, 16));
|
||||
if (ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* p = ImGui::AcceptDragDropPayload("KANBAN_ISSUE")) {
|
||||
std::string id((const char*)p->Data, p->DataSize - 1);
|
||||
patch_issue_status(id, status_key);
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
if (ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* p = ImGui::AcceptDragDropPayload("KANBAN_ISSUE")) {
|
||||
std::string id((const char*)p->Data, p->DataSize - 1);
|
||||
patch_issue_status(id, status_key);
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
}
|
||||
|
||||
void draw_board() {
|
||||
if (!ImGui::Begin(TI_LAYOUT_KANBAN " Board")) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// Snapshot under lock
|
||||
std::vector<Issue> snapshot;
|
||||
{
|
||||
std::lock_guard<std::mutex> g(state().mu);
|
||||
snapshot = state().issues;
|
||||
}
|
||||
|
||||
// Bucket issues by status
|
||||
std::vector<std::vector<const Issue*>> buckets(k_n_columns);
|
||||
int filtered_out = 0;
|
||||
for (const auto& iss : snapshot) {
|
||||
if (!passes_filters(iss)) {
|
||||
filtered_out++;
|
||||
continue;
|
||||
}
|
||||
for (int c = 0; c < k_n_columns; ++c) {
|
||||
if (iss.status == k_columns[c]) {
|
||||
buckets[c].push_back(&iss);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Text("%zu total — %d filtered out", snapshot.size(), filtered_out);
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::BeginTable("board_table", k_n_columns,
|
||||
ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchSame)) {
|
||||
ImGui::TableNextRow();
|
||||
for (int c = 0; c < k_n_columns; ++c) {
|
||||
draw_column(k_columns[c], buckets[c]);
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
} // namespace kanban
|
||||
Reference in New Issue
Block a user