diff --git a/web/src/components/layout/AppNav.tsx b/web/src/components/layout/AppNav.tsx
index 81ff3375..2ebdfb4f 100644
--- a/web/src/components/layout/AppNav.tsx
+++ b/web/src/components/layout/AppNav.tsx
@@ -30,6 +30,7 @@ import { useAppStore } from "@/stores";
import useFeatureAccess from "@/hooks/useFeatureAccess";
import useCampaigns from "@/lib/api/hooks/app/campaigns/useCampaigns";
import useEmails from "@/lib/api/hooks/app/emails/useEmails";
+import useCRMTasks from "@/lib/api/hooks/app/crm/tasks/useCRMTasks";
import { UserNav } from "./UserNav";
import { Logo } from "@/components/svg";
import { cn } from "@/lib/utils";
@@ -44,9 +45,10 @@ interface NavItem {
/** Role gate — when set, sidebar hides the row entirely for non-matching roles. */
rolesAllowed?: "manage";
/** Live indicator key — renders an ambient, realtime activity cluster.
- * Each key has its OWN motif (campaigns = dot-grid, accounts = flame) so
- * the rows stay visually distinct rather than a column of identical loaders. */
- indicator?: "campaigns" | "accounts";
+ * Each key has its OWN motif (campaigns = dot-grid, accounts = flame,
+ * tasks = red attention dot) so the rows stay visually distinct rather
+ * than a column of identical loaders. */
+ indicator?: "campaigns" | "accounts" | "tasks";
}
// Plan badge shown on locked sidebar rows. Plan names + colors come
@@ -90,7 +92,7 @@ const sections: NavSection[] = [
items: [
{ title: "Pipelines", url: "/app/crm/pipelines", icon: GitBranchIcon },
{ title: "Deals", url: "/app/crm/deals", icon: CircleDollarSignIcon },
- { title: "Tasks", url: "/app/crm/tasks", icon: CheckSquareIcon },
+ { title: "Tasks", url: "/app/crm/tasks", icon: CheckSquareIcon, indicator: "tasks" },
],
},
{
@@ -152,6 +154,7 @@ function NavRow({ item }: { item: NavItem }) {
{item.title}
{item.indicator === "campaigns" && !locked && }
{item.indicator === "accounts" && !locked && }
+ {item.indicator === "tasks" && !locked && }
{planBadge ? (
{
+ const tasks = data?.data ?? [];
+ const now = Date.now();
+ return tasks.filter(
+ (t) =>
+ !!t.due_date &&
+ new Date(t.due_date).getTime() < now &&
+ t.status !== "completed" &&
+ t.status !== "cancelled",
+ ).length;
+ }, [data]);
+
+ if (overdue === 0) return null;
+
+ return (
+
+
+
+
+
+
+ {overdue > 99 ? "99+" : overdue}
+
+
+ );
+}
+
function Section({ section, first = false }: { section: NavSection; first?: boolean }) {
return (