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()
|
client = _get_clickup_client()
|
||||||
try:
|
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 = []
|
tasks = []
|
||||||
for t in raw_tasks:
|
for t in raw_tasks:
|
||||||
tasks.append(
|
tasks.append(
|
||||||
|
|
@ -95,7 +95,9 @@ async def get_tasks():
|
||||||
"task_type": t.task_type,
|
"task_type": t.task_type,
|
||||||
"url": t.url,
|
"url": t.url,
|
||||||
"due_date": t.due_date,
|
"due_date": t.due_date,
|
||||||
|
"date_done": t.date_done,
|
||||||
"list_name": t.list_name,
|
"list_name": t.list_name,
|
||||||
|
"tags": t.tags,
|
||||||
"custom_fields": t.custom_fields,
|
"custom_fields": t.custom_fields,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -115,7 +117,7 @@ async def get_tasks_by_company():
|
||||||
data = await get_tasks()
|
data = await get_tasks()
|
||||||
by_company: dict[str, list] = {}
|
by_company: dict[str, list] = {}
|
||||||
for task in data.get("tasks", []):
|
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)
|
by_company.setdefault(company, []).append(task)
|
||||||
|
|
||||||
# Sort companies by task count descending
|
# Sort companies by task count descending
|
||||||
|
|
@ -131,8 +133,41 @@ async def get_tasks_by_company():
|
||||||
@router.get("/tasks/link-building")
|
@router.get("/tasks/link-building")
|
||||||
async def get_link_building_tasks():
|
async def get_link_building_tasks():
|
||||||
"""Link building tasks with KV state merged in."""
|
"""Link building tasks with KV state merged in."""
|
||||||
data = await get_tasks()
|
cached = _get_cached("lb_tasks")
|
||||||
lb_tasks = [t for t in data.get("tasks", []) if t["task_type"] == "Link Building"]
|
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
|
# Merge KV state
|
||||||
if _db:
|
if _db:
|
||||||
|
|
@ -147,20 +182,64 @@ async def get_link_building_tasks():
|
||||||
else:
|
else:
|
||||||
task["kv_state"] = None
|
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] = {}
|
by_company: dict[str, list] = {}
|
||||||
for task in lb_tasks:
|
for task in active_lb:
|
||||||
company = task["custom_fields"].get("Client") or "Unassigned"
|
company = task["custom_fields"].get("Customer") or "Unassigned"
|
||||||
by_company.setdefault(company, []).append(task)
|
by_company.setdefault(company, []).append(task)
|
||||||
|
|
||||||
return {
|
result = {
|
||||||
"total": len(lb_tasks),
|
"total": len(active_lb),
|
||||||
|
"need_cora": need_cora,
|
||||||
|
"recently_completed": recently_completed,
|
||||||
|
"in_progress_not_started": in_progress_not_started,
|
||||||
"companies": [
|
"companies": [
|
||||||
{"name": name, "tasks": tasks, "count": len(tasks)}
|
{"name": name, "tasks": tasks, "count": len(tasks)}
|
||||||
for name, tasks in sorted(by_company.items(), key=lambda x: -len(x[1]))
|
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")
|
@router.get("/tasks/press-releases")
|
||||||
|
|
@ -183,7 +262,7 @@ async def get_press_release_tasks():
|
||||||
|
|
||||||
by_company: dict[str, list] = {}
|
by_company: dict[str, list] = {}
|
||||||
for task in pr_tasks:
|
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)
|
by_company.setdefault(company, []).append(task)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ class ClickUpTask:
|
||||||
custom_fields: dict[str, Any] = field(default_factory=dict)
|
custom_fields: dict[str, Any] = field(default_factory=dict)
|
||||||
list_id: str = ""
|
list_id: str = ""
|
||||||
list_name: str = ""
|
list_name: str = ""
|
||||||
|
tags: list[str] = field(default_factory=list)
|
||||||
|
date_done: str = ""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_api(cls, data: dict, task_type_field_name: str = "Task Type") -> ClickUpTask:
|
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")
|
raw_due = data.get("due_date")
|
||||||
due_date = str(raw_due) if raw_due else ""
|
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(
|
return cls(
|
||||||
id=data["id"],
|
id=data["id"],
|
||||||
name=data.get("name", ""),
|
name=data.get("name", ""),
|
||||||
|
|
@ -71,6 +78,8 @@ class ClickUpTask:
|
||||||
custom_fields=custom_fields,
|
custom_fields=custom_fields,
|
||||||
list_id=data.get("list", {}).get("id", ""),
|
list_id=data.get("list", {}).get("id", ""),
|
||||||
list_name=data.get("list", {}).get("name", ""),
|
list_name=data.get("list", {}).get("name", ""),
|
||||||
|
tags=tags,
|
||||||
|
date_done=date_done,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -100,9 +109,13 @@ class ClickUpClient:
|
||||||
statuses: list[str] | None = None,
|
statuses: list[str] | None = None,
|
||||||
due_date_lt: int | None = None,
|
due_date_lt: int | None = None,
|
||||||
custom_fields: str | None = None,
|
custom_fields: str | None = None,
|
||||||
|
include_closed: bool = False,
|
||||||
) -> list[ClickUpTask]:
|
) -> list[ClickUpTask]:
|
||||||
"""Fetch tasks from a specific list, optionally filtered by status/due date/fields."""
|
"""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:
|
if statuses:
|
||||||
for s in statuses:
|
for s in statuses:
|
||||||
params.setdefault("statuses[]", [])
|
params.setdefault("statuses[]", [])
|
||||||
|
|
@ -170,6 +183,49 @@ class ClickUpClient:
|
||||||
)
|
)
|
||||||
return all_tasks
|
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) ──
|
# ── Write (with retry) ──
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -282,6 +338,41 @@ class ClickUpClient:
|
||||||
|
|
||||||
return list_ids
|
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]:
|
def get_custom_fields(self, list_id: str) -> list[dict]:
|
||||||
"""Get custom fields for a list."""
|
"""Get custom fields for a list."""
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -125,24 +125,35 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Top Link Building Tasks -->
|
<!-- Due Soon (next 14 days) -->
|
||||||
<div class="section section--tight">
|
<div class="section section--tight">
|
||||||
<div class="section__header">
|
<div class="section__header">
|
||||||
<h2 class="section__title"><span class="icon">🔗</span> Link Building Tasks</h2>
|
<h2 class="section__title"><span class="icon">⏰</span> Due Soon</h2>
|
||||||
<span class="section__badge"><a href="#" class="section__link" data-tab="linkbuilding">View all</a></span>
|
<span class="section__badge" id="due-soon-count">-</span>
|
||||||
</div>
|
</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>
|
<p style="padding:1rem;color:var(--text-muted);">Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Top Press Release Tasks -->
|
<!-- This Month (tagged with current month) -->
|
||||||
<div class="section section--tight">
|
<div class="section section--tight">
|
||||||
<div class="section__header">
|
<div class="section__header">
|
||||||
<h2 class="section__title"><span class="icon">📰</span> Press Release Tasks</h2>
|
<h2 class="section__title"><span class="icon">📅</span> This Month</h2>
|
||||||
<span class="section__badge"><a href="#" class="section__link" data-tab="pressreleases">View all</a></span>
|
<span class="section__badge" id="this-month-count">-</span>
|
||||||
</div>
|
</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>
|
<p style="padding:1rem;color:var(--text-muted);">Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -166,6 +177,28 @@
|
||||||
|
|
||||||
<div class="stats-row" id="lb-stats"></div>
|
<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 -->
|
<!-- Company Breakdown -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section__header">
|
<div class="section__header">
|
||||||
|
|
@ -350,7 +383,7 @@ async function loadOverview() {
|
||||||
document.getElementById('badge-pr').textContent = pr.total || 0;
|
document.getElementById('badge-pr').textContent = pr.total || 0;
|
||||||
}
|
}
|
||||||
if (tasks && tasks.tasks) {
|
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;
|
document.getElementById('stat-companies').textContent = companies.size;
|
||||||
const names = [...companies].filter(c => c !== 'Unassigned').slice(0, 4).join(', ');
|
const names = [...companies].filter(c => c !== 'Unassigned').slice(0, 4).join(', ');
|
||||||
document.getElementById('stat-companies-detail').textContent = names || 'None';
|
document.getElementById('stat-companies-detail').textContent = names || 'None';
|
||||||
|
|
@ -360,16 +393,36 @@ async function loadOverview() {
|
||||||
document.getElementById('stat-agents-detail').textContent = 'Configured';
|
document.getElementById('stat-agents-detail').textContent = 'Configured';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Top LB tasks (first 5)
|
// -- Due Soon: tasks due within 14 days --
|
||||||
if (lb && lb.companies) {
|
if (tasks && tasks.tasks) {
|
||||||
const allTasks = lb.companies.flatMap(c => c.tasks);
|
const now = Date.now();
|
||||||
renderTaskTable('overview-lb-table', allTasks.slice(0, 8), true);
|
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
|
// -- Cora Reports Needed --
|
||||||
if (pr && pr.companies) {
|
if (lb && lb.need_cora) {
|
||||||
const allPR = pr.companies.flatMap(c => c.tasks);
|
renderOverviewTable('overview-cora', lb.need_cora, false);
|
||||||
renderPRCards('overview-pr-cards', allPR.slice(0, 3));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health inline
|
// Health inline
|
||||||
|
|
@ -395,7 +448,7 @@ function renderTaskTable(containerId, tasks, compact) {
|
||||||
</tr></thead><tbody>`;
|
</tr></thead><tbody>`;
|
||||||
|
|
||||||
tasks.forEach((t, i) => {
|
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 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);
|
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>
|
html += `<tr>
|
||||||
|
|
@ -411,6 +464,77 @@ function renderTaskTable(containerId, tasks, compact) {
|
||||||
container.innerHTML = html;
|
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) {
|
function renderPRCards(containerId, tasks) {
|
||||||
const container = document.getElementById(containerId);
|
const container = document.getElementById(containerId);
|
||||||
if (!tasks || tasks.length === 0) {
|
if (!tasks || tasks.length === 0) {
|
||||||
|
|
@ -420,7 +544,7 @@ function renderPRCards(containerId, tasks) {
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
tasks.forEach(t => {
|
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 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;
|
const kvState = t.kv_state;
|
||||||
let stateInfo = '';
|
let stateInfo = '';
|
||||||
|
|
@ -495,6 +619,16 @@ async function loadLinkBuilding() {
|
||||||
`;
|
`;
|
||||||
document.getElementById('lb-stats').innerHTML = statsHtml;
|
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
|
// Company breakdown
|
||||||
const grid = document.getElementById('lb-company-grid');
|
const grid = document.getElementById('lb-company-grid');
|
||||||
if (data.companies && data.companies.length > 0) {
|
if (data.companies && data.companies.length > 0) {
|
||||||
|
|
@ -619,7 +753,7 @@ async function loadByCompany() {
|
||||||
function groupByCompany(tasks) {
|
function groupByCompany(tasks) {
|
||||||
const map = {};
|
const map = {};
|
||||||
tasks.forEach(t => {
|
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 };
|
if (!map[co]) map[co] = { name: co, tasks: [], count: 0 };
|
||||||
map[co].tasks.push(t);
|
map[co].tasks.push(t);
|
||||||
map[co].count++;
|
map[co].count++;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue