Skip to content

feat(app): add a tasks page - #142

Open
GuzmanPintos wants to merge 2 commits into
trycompai:mainfrom
GuzmanPintos:gp/tasks-page-upstream
Open

feat(app): add a tasks page#142
GuzmanPintos wants to merge 2 commits into
trycompai:mainfrom
GuzmanPintos:gp/tasks-page-upstream

Conversation

@GuzmanPintos

@GuzmanPintos GuzmanPintos commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Tasks have a page now: Open tasks on the dashboard, or Tasks in the sidebar. It's a normal table, same as Deals. Filter by when something's due or who added it, switch between Open and Done, tick one off without leaving the row, click a row to open whatever it hangs off. You can also edit a task after saving it from the record's timeline, both the text and the due date, and a due date can be cleared now rather than only replaced.

Why: a task you add on a contact with no due date just vanishes. The dashboard only ever showed tasks already past due, and an undated one never is, so the only place it existed was that contact's timeline. Undated is the normal case, too. The date picker doesn't appear until you switch the composer over to Task, and nothing makes you use it. The dashboard card stays as it is. The page is where the rest of them live, undated included, under the No due date filter.

Creating a task still happens on a timeline rather than on this page, since it has to hang off a company, contact or deal.

Screenshots
Dashboard - "Open tasks" button following "Open deals" next to it
image

Tasks page
image

Edit a task
image


Summary by cubic

Adds a paginated Tasks page with search, facets, sorting, and inline complete/edit; switches task due dates to calendar-day strings so due windows are consistent by viewer “today”. Previously, undated tasks were invisible outside timelines; the page now surfaces them and timeline tasks can be edited in place.

  • Replaces activities.myTasks with activities.tasks returning { rows, total, facetCounts }, supporting q, pagination, sorting, and facets: status (open/done), due (overdue/today/week/later/none), and createdBy; requires today to compute windows.
  • Adds activities.update to edit a task’s subject and dueAt with validation; dueAt is a calendar day (YYYY-MM-DD) or null, not an instant.
  • Stores task due dates as UTC‑midnight days and serializes dueAt as YYYY-MM-DD. dashboard.summary now requires today and returns overdue task dueAt as a day string.
  • UI: new /tasks route and rail item; table with tabs (Open/Done), facets (Due, Added by), search, sorting, pagination; inline complete; row click opens the linked record; timeline task editor with day picker; viewer‑day context and relative day labels; dashboard “Open tasks” button.

Required migration actions

  • Replace any activities.myTasks calls with activities.tasks.
  • Pass today (YYYY-MM-DD) to activities.tasks and dashboard.summary.
  • When creating or updating tasks, send dueAt as a day string (YYYY-MM-DD) or null.
  • Use activities.update to edit existing tasks.

Written for commit 47ba82a. Summary will update on new commits.

Review in cubic

The dashboard's only task surface filtered on `dueAt < now`, so a task saved
without a due date — the default in the composer — was invisible everywhere but
its own record timeline. Adds a paginated tasks list on the same DataTable stack
as the other record pages, replacing the unused `activities.myTasks` procedure,
and lets a task's text and due date be edited after it is saved.

Task due dates are stored as UTC-midnight calendar days so a window filter
means the same thing regardless of the viewer's timezone.
@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

@GuzmanPintos is attempting to deploy a commit to the Comp AI - PoC Team on Vercel.

A member of the Team first needs to authorize it.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 issues found across 35 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/app/app/(app)/[slug]/tasks/page.tsx">

<violation number="1" location="apps/app/app/(app)/[slug]/tasks/page.tsx:59">
P2: When the server and viewer are in different time zones, this prefetch uses the server’s calendar day while `ViewerDayProvider` switches to the browser day after hydration. Use one canonical viewer/workspace timezone for both the initial query and provider, or defer this date-dependent prefetch until that day is known.</violation>
</file>

<file name="apps/api/src/activities/activities.service.ts">

<violation number="1" location="apps/api/src/activities/activities.service.ts:253">
P2: When tasks share the selected sort keys, offset pagination can duplicate or omit rows between pages because this ordering has no unique tie-breaker. Append `id` to the resolved task ordering.</violation>

<violation number="2" location="apps/api/src/activities/activities.service.ts:294">
P3: The `tasks()` list performs 11 database queries per request: 1 `findMany`, 1 `count`, plus `taskFacetCounts` which issues a `groupBy`, 2 status counts, and 5 per-window counts (one for each entry of `TASK_WINDOWS`). Each page load of the tasks UI therefore triggers all 11 in parallel regardless of how the facets are used. This scales linearly with the fixed window count and adds a constant high query load to a paginated list endpoint. Consider batching the window counts into a single `groupBy`/aggregation query instead of 5+ separate `count` calls.</violation>
</file>

<file name="apps/app/components/viewer-day.tsx">

<violation number="1" location="apps/app/components/viewer-day.tsx:20">
P2: When the browser and Next server use different time zones, this provider hydrates with the server's `initialDay` and then switches to the browser's `currentDay`. The server-prefetched task/summary data therefore uses the wrong viewer-day window during hydration and can remain visible while the client refetches; derive one viewer day for both SSR and client data, or defer rendering server-prefetched day-dependent data until the client day is established.</violation>
</file>

<file name="apps/app/components/crm/timeline/due-date-button.tsx">

<violation number="1" location="apps/app/components/crm/timeline/due-date-button.tsx:42">
P3: The trigger label formats the selected day with only month and day (`month: "short", day: "numeric"`), so a due date in a different year renders identically to the same month/day this year (e.g. next January due date shows "Jan 3"). Since `DatePickerCalendar` lets users pick any date far into the future, include the year when the selected date is outside the current year to avoid ambiguity.</violation>
</file>

<file name="apps/app/app/(app)/[slug]/page.tsx">

<violation number="1" location="apps/app/app/(app)/[slug]/page.tsx:62">
P2: When the server and viewer are in different time zones around the midnight boundary, `toDay(new Date())` in page.tsx derives the day from the server's clock while the client's ViewerDayProvider later derives it from the viewer's clock. The prefetched dashboard.summary query and the SSR-rendered overdue window are then keyed on the server's day, which can differ from the viewer's actual day, showing the wrong day's data (and wasting the prefetch) until the client re-queries after hydration. Derive `initialDay` from the client consistently (or pass the viewer's day through) so the server-cached `today` matches the client's `today`.</violation>
</file>

<file name="apps/app/components/crm/task-window.ts">

<violation number="1" location="apps/app/components/crm/task-window.ts:1">
P3: The "Due this week" facet is implemented as the next 7 rolling days (tomorrow through today+7) rather than the current calendar week. The label misleads: on a Friday, tasks due the following Thursday are included even though they are not "this week". Consider renaming the option to "Next 7 days" or computing an actual calendar-week window so label and behavior agree.</violation>
</file>

<file name="apps/app/components/crm/timeline/task-editor.tsx">

<violation number="1" location="apps/app/components/crm/timeline/task-editor.tsx:41">
P3: Clicking Save fires `activities.update` even when neither the subject nor the due date changed (e.g., the user opens the editor and hits Save or Ctrl+Enter without edits). This is an unnecessary server write plus a full `cache.activity({ settle: "record" })` invalidation (timeline, counts, tasks, dashboard summary) for a no-op. Skip the mutation when `text === task.subject` (trimmed) and `dueAt === (task.dueAt ?? "")`.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic


const trpc = getServerTrpc();
const queryClient = getServerQueryClient();
const today = toDay(new Date());

@cubic-dev-ai cubic-dev-ai Bot Aug 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the server and viewer are in different time zones, this prefetch uses the server’s calendar day while ViewerDayProvider switches to the browser day after hydration. Use one canonical viewer/workspace timezone for both the initial query and provider, or defer this date-dependent prefetch until that day is known.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/app/(app)/[slug]/tasks/page.tsx, line 59:

<comment>When the server and viewer are in different time zones, this prefetch uses the server’s calendar day while `ViewerDayProvider` switches to the browser day after hydration. Use one canonical viewer/workspace timezone for both the initial query and provider, or defer this date-dependent prefetch until that day is known.</comment>

<file context>
@@ -0,0 +1,77 @@
+
+	const trpc = getServerTrpc();
+	const queryClient = getServerQueryClient();
+	const today = toDay(new Date());
+	await Promise.all([
+		queryClient.prefetchQuery(
</file context>
Fix with cubic

where,
skip,
take,
orderBy: resolveOrderBy(input, TASK_SORTABLE, [

@cubic-dev-ai cubic-dev-ai Bot Aug 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When tasks share the selected sort keys, offset pagination can duplicate or omit rows between pages because this ordering has no unique tie-breaker. Append id to the resolved task ordering.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/activities/activities.service.ts, line 253:

<comment>When tasks share the selected sort keys, offset pagination can duplicate or omit rows between pages because this ordering has no unique tie-breaker. Append `id` to the resolved task ordering.</comment>

<file context>
@@ -179,28 +241,102 @@ export class ActivitiesService {
+				where,
+				skip,
+				take,
+				orderBy: resolveOrderBy(input, TASK_SORTABLE, [
+					{ dueAt: { sort: "asc", nulls: "last" } },
+					{ createdAt: "desc" },
</file context>
Fix with cubic

initialDay: string;
children: ReactNode;
}) {
const day = useSyncExternalStore(subscribe, currentDay, () => initialDay);

@cubic-dev-ai cubic-dev-ai Bot Aug 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the browser and Next server use different time zones, this provider hydrates with the server's initialDay and then switches to the browser's currentDay. The server-prefetched task/summary data therefore uses the wrong viewer-day window during hydration and can remain visible while the client refetches; derive one viewer day for both SSR and client data, or defer rendering server-prefetched day-dependent data until the client day is established.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/components/viewer-day.tsx, line 20:

<comment>When the browser and Next server use different time zones, this provider hydrates with the server's `initialDay` and then switches to the browser's `currentDay`. The server-prefetched task/summary data therefore uses the wrong viewer-day window during hydration and can remain visible while the client refetches; derive one viewer day for both SSR and client data, or defer rendering server-prefetched day-dependent data until the client day is established.</comment>

<file context>
@@ -0,0 +1,59 @@
+	initialDay: string;
+	children: ReactNode;
+}) {
+	const day = useSyncExternalStore(subscribe, currentDay, () => initialDay);
+
+	return (
</file context>
Fix with cubic

]);

const queryClient = getServerQueryClient();
const today = toDay(new Date());

@cubic-dev-ai cubic-dev-ai Bot Aug 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the server and viewer are in different time zones around the midnight boundary, toDay(new Date()) in page.tsx derives the day from the server's clock while the client's ViewerDayProvider later derives it from the viewer's clock. The prefetched dashboard.summary query and the SSR-rendered overdue window are then keyed on the server's day, which can differ from the viewer's actual day, showing the wrong day's data (and wasting the prefetch) until the client re-queries after hydration. Derive initialDay from the client consistently (or pass the viewer's day through) so the server-cached today matches the client's today.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/app/(app)/[slug]/page.tsx, line 62:

<comment>When the server and viewer are in different time zones around the midnight boundary, `toDay(new Date())` in page.tsx derives the day from the server's clock while the client's ViewerDayProvider later derives it from the viewer's clock. The prefetched dashboard.summary query and the SSR-rendered overdue window are then keyed on the server's day, which can differ from the viewer's actual day, showing the wrong day's data (and wasting the prefetch) until the client re-queries after hydration. Derive `initialDay` from the client consistently (or pass the viewer's day through) so the server-cached `today` matches the client's `today`.</comment>

<file context>
@@ -49,19 +52,26 @@ export default function OverviewPage({ searchParams }: PageProps<"/[slug]">) {
 	]);
 
 	const queryClient = getServerQueryClient();
+	const today = toDay(new Date());
 	await queryClient.prefetchQuery(
-		getServerTrpc().dashboard.summary.queryOptions({ scope }),
</file context>
Fix with cubic

<PopoverTrigger asChild>
<InputGroupButton variant="ghost" size="xs" disabled={disabled}>
<Icon icon={CalendarGlyph} data-icon="inline-start" />
{selected ? dueFormat.format(selected) : "Due date"}

@cubic-dev-ai cubic-dev-ai Bot Aug 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The trigger label formats the selected day with only month and day (month: "short", day: "numeric"), so a due date in a different year renders identically to the same month/day this year (e.g. next January due date shows "Jan 3"). Since DatePickerCalendar lets users pick any date far into the future, include the year when the selected date is outside the current year to avoid ambiguity.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/components/crm/timeline/due-date-button.tsx, line 42:

<comment>The trigger label formats the selected day with only month and day (`month: "short", day: "numeric"`), so a due date in a different year renders identically to the same month/day this year (e.g. next January due date shows "Jan 3"). Since `DatePickerCalendar` lets users pick any date far into the future, include the year when the selected date is outside the current year to avoid ambiguity.</comment>

<file context>
@@ -0,0 +1,50 @@
+			<PopoverTrigger asChild>
+				<InputGroupButton variant="ghost" size="xs" disabled={disabled}>
+					<Icon icon={CalendarGlyph} data-icon="inline-start" />
+					{selected ? dueFormat.format(selected) : "Due date"}
+				</InputGroupButton>
+			</PopoverTrigger>
</file context>
Fix with cubic

this.db.activity.count({
where: { ...where, completedAt: { not: null } },
}),
...TASK_WINDOWS.map((window) =>

@cubic-dev-ai cubic-dev-ai Bot Aug 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The tasks() list performs 11 database queries per request: 1 findMany, 1 count, plus taskFacetCounts which issues a groupBy, 2 status counts, and 5 per-window counts (one for each entry of TASK_WINDOWS). Each page load of the tasks UI therefore triggers all 11 in parallel regardless of how the facets are used. This scales linearly with the fixed window count and adds a constant high query load to a paginated list endpoint. Consider batching the window counts into a single groupBy/aggregation query instead of 5+ separate count calls.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/activities/activities.service.ts, line 294:

<comment>The `tasks()` list performs 11 database queries per request: 1 `findMany`, 1 `count`, plus `taskFacetCounts` which issues a `groupBy`, 2 status counts, and 5 per-window counts (one for each entry of `TASK_WINDOWS`). Each page load of the tasks UI therefore triggers all 11 in parallel regardless of how the facets are used. This scales linearly with the fixed window count and adds a constant high query load to a paginated list endpoint. Consider batching the window counts into a single `groupBy`/aggregation query instead of 5+ separate `count` calls.</comment>

<file context>
@@ -179,28 +241,102 @@ export class ActivitiesService {
+			this.db.activity.count({
+				where: { ...where, completedAt: { not: null } },
+			}),
+			...TASK_WINDOWS.map((window) =>
+				this.db.activity.count({
+					where: { ...where, ...taskWindowFilter(window, input.today) },
</file context>
Fix with cubic

@@ -0,0 +1,7 @@
export const TASK_DUE_OPTIONS = [

@cubic-dev-ai cubic-dev-ai Bot Aug 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The "Due this week" facet is implemented as the next 7 rolling days (tomorrow through today+7) rather than the current calendar week. The label misleads: on a Friday, tasks due the following Thursday are included even though they are not "this week". Consider renaming the option to "Next 7 days" or computing an actual calendar-week window so label and behavior agree.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/components/crm/task-window.ts, line 1:

<comment>The "Due this week" facet is implemented as the next 7 rolling days (tomorrow through today+7) rather than the current calendar week. The label misleads: on a Friday, tasks due the following Thursday are included even though they are not "this week". Consider renaming the option to "Next 7 days" or computing an actual calendar-week window so label and behavior agree.</comment>

<file context>
@@ -0,0 +1,7 @@
+export const TASK_DUE_OPTIONS = [
+	{ value: "overdue", label: "Overdue" },
+	{ value: "today", label: "Due today" },
</file context>
Fix with cubic


const submit = () => {
if (text === "" || update.isPending) return;
update.mutate({

@cubic-dev-ai cubic-dev-ai Bot Aug 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Clicking Save fires activities.update even when neither the subject nor the due date changed (e.g., the user opens the editor and hits Save or Ctrl+Enter without edits). This is an unnecessary server write plus a full cache.activity({ settle: "record" }) invalidation (timeline, counts, tasks, dashboard summary) for a no-op. Skip the mutation when text === task.subject (trimmed) and dueAt === (task.dueAt ?? "").

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/components/crm/timeline/task-editor.tsx, line 41:

<comment>Clicking Save fires `activities.update` even when neither the subject nor the due date changed (e.g., the user opens the editor and hits Save or Ctrl+Enter without edits). This is an unnecessary server write plus a full `cache.activity({ settle: "record" })` invalidation (timeline, counts, tasks, dashboard summary) for a no-op. Skip the mutation when `text === task.subject` (trimmed) and `dueAt === (task.dueAt ?? "")`.</comment>

<file context>
@@ -0,0 +1,84 @@
+
+	const submit = () => {
+		if (text === "" || update.isPending) return;
+		update.mutate({
+			id: task.id,
+			subject: text,
</file context>
Fix with cubic

@GuzmanPintos
GuzmanPintos force-pushed the gp/tasks-page-upstream branch from c764426 to d34ff00 Compare August 14, 2026 20:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant