Overhaul dashboard to use Overall lists with focused views
Switch from get_tasks_from_space (all lists) to get_tasks_from_overall_lists (only "Overall" list per folder) to reduce noise. Add tags and date_done fields to ClickUpTask. Redesign Overview tab with Due Soon, This Month, and Cora Reports Needed sections. Add Recently Completed and In Progress Not Started sections to Link Building tab. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>cora-start
parent
76a0200192
commit
0d60b1b516
|
|
@ -84,7 +84,7 @@ async def get_tasks():
|
|||
|
||||
client = _get_clickup_client()
|
||||
try:
|
||||
raw_tasks = client.get_tasks_from_space(_config.clickup.space_id)
|
||||
raw_tasks = client.get_tasks_from_overall_lists(_config.clickup.space_id)
|
||||
tasks = []
|
||||
for t in raw_tasks:
|
||||
tasks.append(
|
||||
|
|
@ -95,7 +95,9 @@ async def get_tasks():
|
|||
"task_type": t.task_type,
|
||||
"url": t.url,
|
||||
"due_date": t.due_date,
|
||||
"date_done": t.date_done,
|
||||
"list_name": t.list_name,
|
||||
"tags": t.tags,
|
||||
"custom_fields": t.custom_fields,
|
||||
}
|
||||
)
|
||||
|
|
@ -115,7 +117,7 @@ async def get_tasks_by_company():
|
|||
data = await get_tasks()
|
||||
by_company: dict[str, list] = {}
|
||||
for task in data.get("tasks", []):
|
||||
company = task["custom_fields"].get("Client") or "Unassigned"
|
||||
company = task["custom_fields"].get("Customer") or "Unassigned"
|
||||
by_company.setdefault(company, []).append(task)
|
||||
|
||||
# Sort companies by task count descending
|
||||
|
|
@ -131,8 +133,41 @@ async def get_tasks_by_company():
|
|||
@router.get("/tasks/link-building")
|
||||
async def get_link_building_tasks():
|
||||
"""Link building tasks with KV state merged in."""
|
||||
data = await get_tasks()
|
||||
lb_tasks = [t for t in data.get("tasks", []) if t["task_type"] == "Link Building"]
|
||||
cached = _get_cached("lb_tasks")
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
if not _config or not _config.clickup.enabled:
|
||||
return {"total": 0, "companies": [], "status_counts": {}}
|
||||
|
||||
client = _get_clickup_client()
|
||||
try:
|
||||
raw_tasks = client.get_tasks_from_overall_lists(
|
||||
_config.clickup.space_id, include_closed=True
|
||||
)
|
||||
except Exception as e:
|
||||
log.error("Failed to fetch LB tasks: %s", e)
|
||||
return {"total": 0, "companies": [], "status_counts": {}, "error": str(e)}
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
lb_tasks = []
|
||||
for t in raw_tasks:
|
||||
if t.task_type != "Link Building":
|
||||
continue
|
||||
task_dict = {
|
||||
"id": t.id,
|
||||
"name": t.name,
|
||||
"status": t.status,
|
||||
"task_type": t.task_type,
|
||||
"url": t.url,
|
||||
"due_date": t.due_date,
|
||||
"date_done": t.date_done,
|
||||
"list_name": t.list_name,
|
||||
"tags": t.tags,
|
||||
"custom_fields": t.custom_fields,
|
||||
}
|
||||
lb_tasks.append(task_dict)
|
||||
|
||||
# Merge KV state
|
||||
if _db:
|
||||
|
|
@ -147,20 +182,64 @@ async def get_link_building_tasks():
|
|||
else:
|
||||
task["kv_state"] = None
|
||||
|
||||
# Group by company
|
||||
# -- Build focused groups --
|
||||
|
||||
# need_cora: status "to do" AND LB Method = "Cora Backlinks"
|
||||
need_cora = [
|
||||
t for t in lb_tasks
|
||||
if t["status"] == "to do"
|
||||
and t["custom_fields"].get("LB Method") == "Cora Backlinks"
|
||||
]
|
||||
|
||||
# recently_completed: closed/complete tasks with date_done in last 7 days
|
||||
seven_days_ago_ms = (time.time() - 7 * 86400) * 1000
|
||||
recently_completed = []
|
||||
for t in lb_tasks:
|
||||
s = t["status"]
|
||||
if not (s.endswith("complete") or "closed" in s or "done" in s):
|
||||
continue
|
||||
if t["date_done"]:
|
||||
try:
|
||||
if int(t["date_done"]) >= seven_days_ago_ms:
|
||||
recently_completed.append(t)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
recently_completed.sort(key=lambda t: int(t.get("date_done") or "0"), reverse=True)
|
||||
|
||||
# in_progress_not_started: status "in progress" but no meaningful KV state
|
||||
early_states = {"", "approved", "awaiting_approval"}
|
||||
in_progress_not_started = []
|
||||
for t in lb_tasks:
|
||||
if t["status"] != "in progress":
|
||||
continue
|
||||
kv = t.get("kv_state")
|
||||
if kv is None or kv.get("state", "") in early_states:
|
||||
in_progress_not_started.append(t)
|
||||
|
||||
# Group by company (exclude closed from the active list for the grid)
|
||||
closed_statuses = {"complete", "closed", "done"}
|
||||
active_lb = [
|
||||
t for t in lb_tasks
|
||||
if not any(kw in t["status"] for kw in closed_statuses)
|
||||
]
|
||||
by_company: dict[str, list] = {}
|
||||
for task in lb_tasks:
|
||||
company = task["custom_fields"].get("Client") or "Unassigned"
|
||||
for task in active_lb:
|
||||
company = task["custom_fields"].get("Customer") or "Unassigned"
|
||||
by_company.setdefault(company, []).append(task)
|
||||
|
||||
return {
|
||||
"total": len(lb_tasks),
|
||||
result = {
|
||||
"total": len(active_lb),
|
||||
"need_cora": need_cora,
|
||||
"recently_completed": recently_completed,
|
||||
"in_progress_not_started": in_progress_not_started,
|
||||
"companies": [
|
||||
{"name": name, "tasks": tasks, "count": len(tasks)}
|
||||
for name, tasks in sorted(by_company.items(), key=lambda x: -len(x[1]))
|
||||
],
|
||||
"status_counts": _count_statuses(lb_tasks),
|
||||
"status_counts": _count_statuses(active_lb),
|
||||
}
|
||||
_set_cached("lb_tasks", result)
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/tasks/press-releases")
|
||||
|
|
@ -183,7 +262,7 @@ async def get_press_release_tasks():
|
|||
|
||||
by_company: dict[str, list] = {}
|
||||
for task in pr_tasks:
|
||||
company = task["custom_fields"].get("Client") or "Unassigned"
|
||||
company = task["custom_fields"].get("Customer") or "Unassigned"
|
||||
by_company.setdefault(company, []).append(task)
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ class ClickUpTask:
|
|||
custom_fields: dict[str, Any] = field(default_factory=dict)
|
||||
list_id: str = ""
|
||||
list_name: str = ""
|
||||
tags: list[str] = field(default_factory=list)
|
||||
date_done: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_api(cls, data: dict, task_type_field_name: str = "Task Type") -> ClickUpTask:
|
||||
|
|
@ -60,6 +62,11 @@ class ClickUpTask:
|
|||
raw_due = data.get("due_date")
|
||||
due_date = str(raw_due) if raw_due else ""
|
||||
|
||||
tags = [tag["name"] for tag in data.get("tags", [])]
|
||||
|
||||
raw_done = data.get("date_done") or data.get("date_closed")
|
||||
date_done = str(raw_done) if raw_done else ""
|
||||
|
||||
return cls(
|
||||
id=data["id"],
|
||||
name=data.get("name", ""),
|
||||
|
|
@ -71,6 +78,8 @@ class ClickUpTask:
|
|||
custom_fields=custom_fields,
|
||||
list_id=data.get("list", {}).get("id", ""),
|
||||
list_name=data.get("list", {}).get("name", ""),
|
||||
tags=tags,
|
||||
date_done=date_done,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -100,9 +109,13 @@ class ClickUpClient:
|
|||
statuses: list[str] | None = None,
|
||||
due_date_lt: int | None = None,
|
||||
custom_fields: str | None = None,
|
||||
include_closed: bool = False,
|
||||
) -> list[ClickUpTask]:
|
||||
"""Fetch tasks from a specific list, optionally filtered by status/due date/fields."""
|
||||
params: dict[str, Any] = {"include_closed": "false", "subtasks": "true"}
|
||||
params: dict[str, Any] = {
|
||||
"include_closed": "true" if include_closed else "false",
|
||||
"subtasks": "true",
|
||||
}
|
||||
if statuses:
|
||||
for s in statuses:
|
||||
params.setdefault("statuses[]", [])
|
||||
|
|
@ -170,6 +183,49 @@ class ClickUpClient:
|
|||
)
|
||||
return all_tasks
|
||||
|
||||
def get_tasks_from_overall_lists(
|
||||
self,
|
||||
space_id: str,
|
||||
statuses: list[str] | None = None,
|
||||
due_date_lt: int | None = None,
|
||||
custom_fields: str | None = None,
|
||||
include_closed: bool = False,
|
||||
) -> list[ClickUpTask]:
|
||||
"""Fetch tasks only from 'Overall' lists in each folder.
|
||||
|
||||
This is the dashboard-specific alternative to get_tasks_from_space,
|
||||
which hits every list and returns too much noise.
|
||||
"""
|
||||
all_tasks: list[ClickUpTask] = []
|
||||
overall_ids: list[str] = []
|
||||
|
||||
try:
|
||||
folders = self.get_folders(space_id)
|
||||
for folder in folders:
|
||||
for lst in folder["lists"]:
|
||||
if lst["name"].lower() == "overall":
|
||||
overall_ids.append(lst["id"])
|
||||
except httpx.HTTPStatusError as e:
|
||||
log.warning("Failed to fetch folders for space %s: %s", space_id, e)
|
||||
return []
|
||||
|
||||
for list_id in overall_ids:
|
||||
try:
|
||||
tasks = self.get_tasks(
|
||||
list_id, statuses, due_date_lt, custom_fields, include_closed
|
||||
)
|
||||
all_tasks.extend(tasks)
|
||||
except httpx.HTTPStatusError as e:
|
||||
log.warning("Failed to fetch tasks from list %s: %s", list_id, e)
|
||||
|
||||
log.info(
|
||||
"Found %d tasks across %d Overall lists in space %s",
|
||||
len(all_tasks),
|
||||
len(overall_ids),
|
||||
space_id,
|
||||
)
|
||||
return all_tasks
|
||||
|
||||
# ── Write (with retry) ──
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -282,6 +338,41 @@ class ClickUpClient:
|
|||
|
||||
return list_ids
|
||||
|
||||
def get_folders(self, space_id: str) -> list[dict]:
|
||||
"""Return folders in a space with their lists.
|
||||
|
||||
Each dict has keys: id, name, lists (list of {id, name}).
|
||||
"""
|
||||
resp = self._client.get(f"/space/{space_id}/folder")
|
||||
resp.raise_for_status()
|
||||
folders = []
|
||||
for f in resp.json().get("folders", []):
|
||||
lists = [{"id": lst["id"], "name": lst["name"]} for lst in f.get("lists", [])]
|
||||
folders.append({"id": f["id"], "name": f["name"], "lists": lists})
|
||||
return folders
|
||||
|
||||
def set_custom_field_value(self, task_id: str, field_id: str, value: Any) -> bool:
|
||||
"""Set a custom field value on a task.
|
||||
|
||||
For dropdowns, *value* should be the option UUID string.
|
||||
"""
|
||||
try:
|
||||
|
||||
def _call():
|
||||
resp = self._client.post(
|
||||
f"/task/{task_id}/field/{field_id}",
|
||||
json={"value": value},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp
|
||||
|
||||
self._retry(_call)
|
||||
log.info("Set field %s on task %s", field_id, task_id)
|
||||
return True
|
||||
except (httpx.TransportError, httpx.HTTPStatusError) as e:
|
||||
log.error("Failed to set field %s on task %s: %s", field_id, task_id, e)
|
||||
return False
|
||||
|
||||
def get_custom_fields(self, list_id: str) -> list[dict]:
|
||||
"""Get custom fields for a list."""
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -125,24 +125,35 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Link Building Tasks -->
|
||||
<!-- Due Soon (next 14 days) -->
|
||||
<div class="section section--tight">
|
||||
<div class="section__header">
|
||||
<h2 class="section__title"><span class="icon">🔗</span> Link Building Tasks</h2>
|
||||
<span class="section__badge"><a href="#" class="section__link" data-tab="linkbuilding">View all</a></span>
|
||||
<h2 class="section__title"><span class="icon">⏰</span> Due Soon</h2>
|
||||
<span class="section__badge" id="due-soon-count">-</span>
|
||||
</div>
|
||||
<div class="task-table-wrap task-table-wrap--compact" id="overview-lb-table">
|
||||
<div class="task-table-wrap task-table-wrap--compact" id="overview-due-soon">
|
||||
<p style="padding:1rem;color:var(--text-muted);">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Press Release Tasks -->
|
||||
<!-- This Month (tagged with current month) -->
|
||||
<div class="section section--tight">
|
||||
<div class="section__header">
|
||||
<h2 class="section__title"><span class="icon">📰</span> Press Release Tasks</h2>
|
||||
<span class="section__badge"><a href="#" class="section__link" data-tab="pressreleases">View all</a></span>
|
||||
<h2 class="section__title"><span class="icon">📅</span> This Month</h2>
|
||||
<span class="section__badge" id="this-month-count">-</span>
|
||||
</div>
|
||||
<div id="overview-pr-cards">
|
||||
<div class="task-table-wrap task-table-wrap--compact" id="overview-this-month">
|
||||
<p style="padding:1rem;color:var(--text-muted);">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cora Reports Needed -->
|
||||
<div class="section section--tight">
|
||||
<div class="section__header">
|
||||
<h2 class="section__title"><span class="icon">📋</span> Cora Reports Needed</h2>
|
||||
<span class="section__badge"><a href="#" class="section__link" data-tab="linkbuilding">View LB</a></span>
|
||||
</div>
|
||||
<div class="task-table-wrap task-table-wrap--compact" id="overview-cora">
|
||||
<p style="padding:1rem;color:var(--text-muted);">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -166,6 +177,28 @@
|
|||
|
||||
<div class="stats-row" id="lb-stats"></div>
|
||||
|
||||
<!-- Recently Completed (Past 7 Days) -->
|
||||
<div class="section">
|
||||
<div class="section__header">
|
||||
<h2 class="section__title"><span class="icon">✅</span> Recently Completed (Past 7 Days)</h2>
|
||||
<span class="section__badge" id="lb-recent-count">-</span>
|
||||
</div>
|
||||
<div class="task-table-wrap" id="lb-recent-table">
|
||||
<p style="padding:1rem;color:var(--text-muted);">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- In Progress - Not Started -->
|
||||
<div class="section">
|
||||
<div class="section__header">
|
||||
<h2 class="section__title"><span class="icon">⚠</span> In Progress — Not Started</h2>
|
||||
<span class="section__badge" id="lb-not-started-count">-</span>
|
||||
</div>
|
||||
<div class="task-table-wrap" id="lb-not-started-table">
|
||||
<p style="padding:1rem;color:var(--text-muted);">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Company Breakdown -->
|
||||
<div class="section">
|
||||
<div class="section__header">
|
||||
|
|
@ -350,7 +383,7 @@ async function loadOverview() {
|
|||
document.getElementById('badge-pr').textContent = pr.total || 0;
|
||||
}
|
||||
if (tasks && tasks.tasks) {
|
||||
const companies = new Set(tasks.tasks.map(t => t.custom_fields?.Client || 'Unassigned'));
|
||||
const companies = new Set(tasks.tasks.map(t => t.custom_fields?.Customer || 'Unassigned'));
|
||||
document.getElementById('stat-companies').textContent = companies.size;
|
||||
const names = [...companies].filter(c => c !== 'Unassigned').slice(0, 4).join(', ');
|
||||
document.getElementById('stat-companies-detail').textContent = names || 'None';
|
||||
|
|
@ -360,16 +393,36 @@ async function loadOverview() {
|
|||
document.getElementById('stat-agents-detail').textContent = 'Configured';
|
||||
}
|
||||
|
||||
// Top LB tasks (first 5)
|
||||
if (lb && lb.companies) {
|
||||
const allTasks = lb.companies.flatMap(c => c.tasks);
|
||||
renderTaskTable('overview-lb-table', allTasks.slice(0, 8), true);
|
||||
// -- Due Soon: tasks due within 14 days --
|
||||
if (tasks && tasks.tasks) {
|
||||
const now = Date.now();
|
||||
const fourteenDays = 14 * 24 * 60 * 60 * 1000;
|
||||
const dueSoon = tasks.tasks
|
||||
.filter(t => {
|
||||
if (!t.due_date) return false;
|
||||
const due = parseInt(t.due_date, 10);
|
||||
return due > now && due <= now + fourteenDays;
|
||||
})
|
||||
.sort((a, b) => parseInt(a.due_date) - parseInt(b.due_date));
|
||||
const dueSoonIds = new Set(dueSoon.map(t => t.id));
|
||||
document.getElementById('due-soon-count').textContent = dueSoon.length;
|
||||
renderOverviewTable('overview-due-soon', dueSoon, true);
|
||||
|
||||
// -- This Month: tasks tagged with current month tag, excluding due-soon --
|
||||
const monthNames = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'];
|
||||
const d = new Date();
|
||||
const monthTag = monthNames[d.getMonth()] + String(d.getFullYear()).slice(2);
|
||||
const thisMonth = tasks.tasks.filter(t => {
|
||||
if (dueSoonIds.has(t.id)) return false;
|
||||
return (t.tags || []).some(tag => tag.toLowerCase() === monthTag);
|
||||
});
|
||||
document.getElementById('this-month-count').textContent = thisMonth.length;
|
||||
renderOverviewTable('overview-this-month', thisMonth, false);
|
||||
}
|
||||
|
||||
// Top PR tasks
|
||||
if (pr && pr.companies) {
|
||||
const allPR = pr.companies.flatMap(c => c.tasks);
|
||||
renderPRCards('overview-pr-cards', allPR.slice(0, 3));
|
||||
// -- Cora Reports Needed --
|
||||
if (lb && lb.need_cora) {
|
||||
renderOverviewTable('overview-cora', lb.need_cora, false);
|
||||
}
|
||||
|
||||
// Health inline
|
||||
|
|
@ -395,7 +448,7 @@ function renderTaskTable(containerId, tasks, compact) {
|
|||
</tr></thead><tbody>`;
|
||||
|
||||
tasks.forEach((t, i) => {
|
||||
const company = t.custom_fields?.Client || 'Unassigned';
|
||||
const company = t.custom_fields?.Customer || 'Unassigned';
|
||||
const keyword = t.custom_fields?.Keyword || '';
|
||||
const link = t.url ? `<a href="${esc(t.url)}" target="_blank" style="color:var(--cream-light);text-decoration:none;">${esc(t.name)}</a>` : esc(t.name);
|
||||
html += `<tr>
|
||||
|
|
@ -411,6 +464,77 @@ function renderTaskTable(containerId, tasks, compact) {
|
|||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderRecentTable(containerId, tasks) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!tasks || tasks.length === 0) {
|
||||
container.innerHTML = '<p style="padding:1rem;color:var(--text-muted);">No recently completed tasks.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `<table class="task-table">
|
||||
<thead><tr>
|
||||
<th>#</th><th>Task</th><th>Company</th><th>Keyword</th><th>Completed</th>
|
||||
</tr></thead><tbody>`;
|
||||
|
||||
tasks.forEach((t, i) => {
|
||||
const company = t.custom_fields?.Customer || 'Unassigned';
|
||||
const keyword = t.custom_fields?.Keyword || '';
|
||||
const link = t.url ? `<a href="${esc(t.url)}" target="_blank" style="color:var(--cream-light);text-decoration:none;">${esc(t.name)}</a>` : esc(t.name);
|
||||
let doneDate = '-';
|
||||
if (t.date_done) {
|
||||
const d = new Date(parseInt(t.date_done, 10));
|
||||
doneDate = d.toLocaleDateString('en-US', {month:'short', day:'numeric', year:'numeric'});
|
||||
}
|
||||
html += `<tr>
|
||||
<td class="task-table__num">${i + 1}</td>
|
||||
<td class="task-table__title">${link}</td>
|
||||
<td class="task-table__company">${esc(company)}</td>
|
||||
<td class="task-table__keyword">${esc(keyword)}</td>
|
||||
<td style="white-space:nowrap;">${esc(doneDate)}</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderOverviewTable(containerId, tasks, showDueDate) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!tasks || tasks.length === 0) {
|
||||
container.innerHTML = '<p style="padding:1rem;color:var(--text-muted);">None right now.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const dueDateHeader = showDueDate ? '<th>Due</th>' : '';
|
||||
let html = `<table class="task-table task-table--dense">
|
||||
<thead><tr>
|
||||
<th>#</th><th>Task</th><th>Company</th><th>Type</th><th>Status</th>${dueDateHeader}
|
||||
</tr></thead><tbody>`;
|
||||
|
||||
tasks.forEach((t, i) => {
|
||||
const company = t.custom_fields?.Customer || 'Unassigned';
|
||||
const link = t.url ? `<a href="${esc(t.url)}" target="_blank" style="color:var(--cream-light);text-decoration:none;">${esc(t.name)}</a>` : esc(t.name);
|
||||
let dueDateCell = '';
|
||||
if (showDueDate && t.due_date) {
|
||||
const d = new Date(parseInt(t.due_date, 10));
|
||||
dueDateCell = `<td style="white-space:nowrap;">${d.toLocaleDateString('en-US', {month:'short', day:'numeric'})}</td>`;
|
||||
} else if (showDueDate) {
|
||||
dueDateCell = '<td>-</td>';
|
||||
}
|
||||
html += `<tr>
|
||||
<td class="task-table__num">${i + 1}</td>
|
||||
<td class="task-table__title">${link}</td>
|
||||
<td class="task-table__company">${esc(company)}</td>
|
||||
<td class="task-table__company">${esc(t.task_type || '-')}</td>
|
||||
<td class="task-table__status">${statusPill(t.status)}</td>
|
||||
${dueDateCell}
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderPRCards(containerId, tasks) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!tasks || tasks.length === 0) {
|
||||
|
|
@ -420,7 +544,7 @@ function renderPRCards(containerId, tasks) {
|
|||
|
||||
let html = '';
|
||||
tasks.forEach(t => {
|
||||
const company = t.custom_fields?.Client || 'Unassigned';
|
||||
const company = t.custom_fields?.Customer || 'Unassigned';
|
||||
const link = t.url ? ` <a href="${esc(t.url)}" target="_blank" style="color:var(--gold-light);font-size:0.75rem;">[ClickUp]</a>` : '';
|
||||
const kvState = t.kv_state;
|
||||
let stateInfo = '';
|
||||
|
|
@ -495,6 +619,16 @@ async function loadLinkBuilding() {
|
|||
`;
|
||||
document.getElementById('lb-stats').innerHTML = statsHtml;
|
||||
|
||||
// Recently Completed
|
||||
const recent = data.recently_completed || [];
|
||||
document.getElementById('lb-recent-count').textContent = recent.length;
|
||||
renderRecentTable('lb-recent-table', recent);
|
||||
|
||||
// In Progress - Not Started
|
||||
const notStarted = data.in_progress_not_started || [];
|
||||
document.getElementById('lb-not-started-count').textContent = notStarted.length;
|
||||
renderTaskTable('lb-not-started-table', notStarted, false);
|
||||
|
||||
// Company breakdown
|
||||
const grid = document.getElementById('lb-company-grid');
|
||||
if (data.companies && data.companies.length > 0) {
|
||||
|
|
@ -619,7 +753,7 @@ async function loadByCompany() {
|
|||
function groupByCompany(tasks) {
|
||||
const map = {};
|
||||
tasks.forEach(t => {
|
||||
const co = t.custom_fields?.Client || 'Unassigned';
|
||||
const co = t.custom_fields?.Customer || 'Unassigned';
|
||||
if (!map[co]) map[co] = { name: co, tasks: [], count: 0 };
|
||||
map[co].tasks.push(t);
|
||||
map[co].count++;
|
||||
|
|
|
|||
Loading…
Reference in New Issue