feat(app): add a tasks page - #142
Conversation
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.
|
@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. |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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>
| where, | ||
| skip, | ||
| take, | ||
| orderBy: resolveOrderBy(input, TASK_SORTABLE, [ |
There was a problem hiding this comment.
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>
| initialDay: string; | ||
| children: ReactNode; | ||
| }) { | ||
| const day = useSyncExternalStore(subscribe, currentDay, () => initialDay); |
There was a problem hiding this comment.
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>
| ]); | ||
|
|
||
| const queryClient = getServerQueryClient(); | ||
| const today = toDay(new Date()); |
There was a problem hiding this comment.
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>
| <PopoverTrigger asChild> | ||
| <InputGroupButton variant="ghost" size="xs" disabled={disabled}> | ||
| <Icon icon={CalendarGlyph} data-icon="inline-start" /> | ||
| {selected ? dueFormat.format(selected) : "Due date"} |
There was a problem hiding this comment.
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>
| this.db.activity.count({ | ||
| where: { ...where, completedAt: { not: null } }, | ||
| }), | ||
| ...TASK_WINDOWS.map((window) => |
There was a problem hiding this comment.
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>
| @@ -0,0 +1,7 @@ | |||
| export const TASK_DUE_OPTIONS = [ | |||
There was a problem hiding this comment.
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>
|
|
||
| const submit = () => { | ||
| if (text === "" || update.isPending) return; | ||
| update.mutate({ |
There was a problem hiding this comment.
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>
c764426 to
d34ff00
Compare
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
Tasks page

Edit a task

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.
activities.myTaskswithactivities.tasksreturning{ rows, total, facetCounts }, supportingq, pagination, sorting, and facets:status(open/done),due(overdue/today/week/later/none), andcreatedBy; requirestodayto compute windows.activities.updateto edit a task’ssubjectanddueAtwith validation;dueAtis a calendar day (YYYY-MM-DD) or null, not an instant.dueAtasYYYY-MM-DD.dashboard.summarynow requirestodayand returns overdue taskdueAtas a day string./tasksroute 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
activities.myTaskscalls withactivities.tasks.today(YYYY-MM-DD) toactivities.tasksanddashboard.summary.dueAtas a day string (YYYY-MM-DD) or null.activities.updateto edit existing tasks.Written for commit 47ba82a. Summary will update on new commits.