From 1a7f5d706a3afeca309e28e9268e149659c14902 Mon Sep 17 00:00:00 2001 From: Gabriel Sancho Date: Fri, 27 Mar 2026 01:10:15 -0300 Subject: [PATCH] feat(frontend): show inactive tasks on entity page Return all tasks for an entity so inactive items remain visible in the entity detail view while global task listings stay active-only. Add inactive task styling and coverage for the entity page state. --- frontend/src/__tests__/api/tasksApi.test.ts | 9 +- .../__tests__/pages/EntityDetailPage.test.tsx | 35 ++ frontend/src/api/tasksApi.ts | 314 +++++++++--------- frontend/src/pages/EntityDetailPage.tsx | 28 +- 4 files changed, 223 insertions(+), 163 deletions(-) diff --git a/frontend/src/__tests__/api/tasksApi.test.ts b/frontend/src/__tests__/api/tasksApi.test.ts index dd66772..7005815 100644 --- a/frontend/src/__tests__/api/tasksApi.test.ts +++ b/frontend/src/__tests__/api/tasksApi.test.ts @@ -54,7 +54,14 @@ describe('tasksApi', () => { it('should_returnTasksForEntity_when_getTasksByEntityCalled', async () => { localStorage.setItem('condado:entity-tasks', JSON.stringify([taskOne, taskTwo])) - await expect(getTasksByEntity('entity-1')).resolves.toEqual([taskOne]) + await expect(getTasksByEntity('entity-1')).resolves.toEqual([ + taskOne, + { + ...taskOne, + id: 'task-3', + active: false, + }, + ]) }) it('should_hideInactiveTasks_when_getAllTasksCalled', async () => { diff --git a/frontend/src/__tests__/pages/EntityDetailPage.test.tsx b/frontend/src/__tests__/pages/EntityDetailPage.test.tsx index b9d6961..4bf02c5 100644 --- a/frontend/src/__tests__/pages/EntityDetailPage.test.tsx +++ b/frontend/src/__tests__/pages/EntityDetailPage.test.tsx @@ -64,4 +64,39 @@ describe('EntityDetailPage', () => { ) }) }) + + it('should_renderInactiveTasksWithStatus_when_entityHasInactiveTasks', async () => { + vi.mocked(tasksApi.getEmailLookbackLabel).mockReturnValue('Last week') + + vi.mocked(entitiesApi.getEntity).mockResolvedValue({ + id: 'entity-1', + name: 'Entity A', + email: 'a@a.com', + jobTitle: 'Ops', + personality: 'Formal', + scheduleCron: '0 9 * * 1', + contextWindowDays: 3, + active: true, + createdAt: '', + }) + vi.mocked(tasksApi.getTasksByEntity).mockResolvedValue([ + { + id: 'task-2', + entityId: 'entity-1', + name: 'Retired Task', + prompt: 'Archive the sandwich minutes', + scheduleCron: '0 9 * * 1', + emailLookback: 'last_week', + active: false, + createdAt: '2026-03-26T10:00:00Z', + }, + ]) + + render(, { wrapper }) + + await waitFor(() => { + expect(screen.getByText(/Retired Task/i)).toBeInTheDocument() + expect(screen.getByText(/Inactive/i)).toBeInTheDocument() + }) + }) }) diff --git a/frontend/src/api/tasksApi.ts b/frontend/src/api/tasksApi.ts index 92c5621..fee8329 100644 --- a/frontend/src/api/tasksApi.ts +++ b/frontend/src/api/tasksApi.ts @@ -1,158 +1,158 @@ -const STORAGE_KEY = 'condado:entity-tasks' - -export type EmailLookback = 'last_day' | 'last_week' | 'last_month' - -export interface EntityTaskResponse { - id: string - entityId: string - name: string - prompt: string - scheduleCron: string - emailLookback: EmailLookback - active: boolean - createdAt: string -} - -export interface EntityTaskCreateDto { - entityId: string - name: string - prompt: string - scheduleCron: string - emailLookback: EmailLookback -} - -export type EntityTaskUpdateDto = EntityTaskCreateDto - -function readTasks(): EntityTaskResponse[] { - const raw = localStorage.getItem(STORAGE_KEY) - if (!raw) return [] - - try { - return (JSON.parse(raw) as Array & { active?: boolean }>).map( - (task) => ({ - ...task, - active: task.active ?? true, - }) - ) - } catch { - return [] - } -} - -function writeTasks(tasks: EntityTaskResponse[]): void { - localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks)) -} - -export function getEmailLookbackLabel(value: EmailLookback): string { - if (value === 'last_day') return 'Last 24 hours' - if (value === 'last_month') return 'Last month' - return 'Last week' -} - -/** Simulates a task preview generated from the configured prompt. */ -export async function generateTaskPreview(data: EntityTaskCreateDto): Promise { - return [ - `SUBJECT: Internal Alignment Update - ${data.name}`, - 'BODY:', - `Dear Team,`, - '', - `In strict accordance with our communication standards, this message was generated from the prompt: "${data.prompt}".`, - `Context window considered: ${getEmailLookbackLabel(data.emailLookback)}.`, - 'Operational interpretation: please proceed casually, but with ceremonial seriousness.', - '', - 'Regards,', - 'Automated Task Preview', - ].join('\n') -} - -/** Returns all scheduled tasks currently configured in local storage. */ -export async function getAllTasks(): Promise { - return readTasks().filter((task) => task.active) -} - -/** Returns scheduled tasks for a specific entity. */ -export async function getTasksByEntity(entityId: string): Promise { - return readTasks().filter((task) => task.entityId === entityId && task.active) -} - -/** Returns one scheduled task by identifier. */ -export async function getTask(taskId: string): Promise { - return readTasks().find((task) => task.id === taskId) ?? null -} - -/** Creates a scheduled task in local storage. */ -export async function createTask(data: EntityTaskCreateDto): Promise { - const current = readTasks() - const task: EntityTaskResponse = { - ...data, - id: crypto.randomUUID(), - active: true, - createdAt: new Date().toISOString(), - } - - current.push(task) - writeTasks(current) - return task -} - -/** Updates one scheduled task in local storage. */ -export async function updateTask( - taskId: string, - data: EntityTaskUpdateDto -): Promise { - const current = readTasks() - const existingTask = current.find((task) => task.id === taskId) - - if (!existingTask) { - return null - } - - const updatedTask: EntityTaskResponse = { - ...existingTask, - ...data, - } - - writeTasks(current.map((task) => (task.id === taskId ? updatedTask : task))) - return updatedTask -} - -/** Marks one scheduled task as inactive in local storage. */ -export async function inactivateTask(taskId: string): Promise { - const current = readTasks() - const existingTask = current.find((task) => task.id === taskId) - - if (!existingTask) { - return null - } - - const updatedTask: EntityTaskResponse = { - ...existingTask, - active: false, - } - - writeTasks(current.map((task) => (task.id === taskId ? updatedTask : task))) - return updatedTask -} - -/** Marks one scheduled task as active in local storage. */ -export async function activateTask(taskId: string): Promise { - const current = readTasks() - const existingTask = current.find((task) => task.id === taskId) - - if (!existingTask) { - return null - } - - const updatedTask: EntityTaskResponse = { - ...existingTask, - active: true, - } - - writeTasks(current.map((task) => (task.id === taskId ? updatedTask : task))) - return updatedTask -} +const STORAGE_KEY = 'condado:entity-tasks' -/** Deletes one scheduled task from local storage. */ -export async function deleteTask(taskId: string): Promise { - writeTasks(readTasks().filter((task) => task.id !== taskId)) -} +export type EmailLookback = 'last_day' | 'last_week' | 'last_month' + +export interface EntityTaskResponse { + id: string + entityId: string + name: string + prompt: string + scheduleCron: string + emailLookback: EmailLookback + active: boolean + createdAt: string +} + +export interface EntityTaskCreateDto { + entityId: string + name: string + prompt: string + scheduleCron: string + emailLookback: EmailLookback +} + +export type EntityTaskUpdateDto = EntityTaskCreateDto + +function readTasks(): EntityTaskResponse[] { + const raw = localStorage.getItem(STORAGE_KEY) + if (!raw) return [] + + try { + return (JSON.parse(raw) as Array & { active?: boolean }>).map( + (task) => ({ + ...task, + active: task.active ?? true, + }) + ) + } catch { + return [] + } +} + +function writeTasks(tasks: EntityTaskResponse[]): void { + localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks)) +} + +export function getEmailLookbackLabel(value: EmailLookback): string { + if (value === 'last_day') return 'Last 24 hours' + if (value === 'last_month') return 'Last month' + return 'Last week' +} + +/** Simulates a task preview generated from the configured prompt. */ +export async function generateTaskPreview(data: EntityTaskCreateDto): Promise { + return [ + `SUBJECT: Internal Alignment Update - ${data.name}`, + 'BODY:', + `Dear Team,`, + '', + `In strict accordance with our communication standards, this message was generated from the prompt: "${data.prompt}".`, + `Context window considered: ${getEmailLookbackLabel(data.emailLookback)}.`, + 'Operational interpretation: please proceed casually, but with ceremonial seriousness.', + '', + 'Regards,', + 'Automated Task Preview', + ].join('\n') +} + +/** Returns all scheduled tasks currently configured in local storage. */ +export async function getAllTasks(): Promise { + return readTasks().filter((task) => task.active) +} + +/** Returns scheduled tasks for a specific entity. */ +export async function getTasksByEntity(entityId: string): Promise { + return readTasks().filter((task) => task.entityId === entityId) +} + +/** Returns one scheduled task by identifier. */ +export async function getTask(taskId: string): Promise { + return readTasks().find((task) => task.id === taskId) ?? null +} + +/** Creates a scheduled task in local storage. */ +export async function createTask(data: EntityTaskCreateDto): Promise { + const current = readTasks() + const task: EntityTaskResponse = { + ...data, + id: crypto.randomUUID(), + active: true, + createdAt: new Date().toISOString(), + } + + current.push(task) + writeTasks(current) + return task +} + +/** Updates one scheduled task in local storage. */ +export async function updateTask( + taskId: string, + data: EntityTaskUpdateDto +): Promise { + const current = readTasks() + const existingTask = current.find((task) => task.id === taskId) + + if (!existingTask) { + return null + } + + const updatedTask: EntityTaskResponse = { + ...existingTask, + ...data, + } + + writeTasks(current.map((task) => (task.id === taskId ? updatedTask : task))) + return updatedTask +} + +/** Marks one scheduled task as inactive in local storage. */ +export async function inactivateTask(taskId: string): Promise { + const current = readTasks() + const existingTask = current.find((task) => task.id === taskId) + + if (!existingTask) { + return null + } + + const updatedTask: EntityTaskResponse = { + ...existingTask, + active: false, + } + + writeTasks(current.map((task) => (task.id === taskId ? updatedTask : task))) + return updatedTask +} + +/** Marks one scheduled task as active in local storage. */ +export async function activateTask(taskId: string): Promise { + const current = readTasks() + const existingTask = current.find((task) => task.id === taskId) + + if (!existingTask) { + return null + } + + const updatedTask: EntityTaskResponse = { + ...existingTask, + active: true, + } + + writeTasks(current.map((task) => (task.id === taskId ? updatedTask : task))) + return updatedTask +} + +/** Deletes one scheduled task from local storage. */ +export async function deleteTask(taskId: string): Promise { + writeTasks(readTasks().filter((task) => task.id !== taskId)) +} diff --git a/frontend/src/pages/EntityDetailPage.tsx b/frontend/src/pages/EntityDetailPage.tsx index 11d6f8e..670e087 100644 --- a/frontend/src/pages/EntityDetailPage.tsx +++ b/frontend/src/pages/EntityDetailPage.tsx @@ -61,16 +61,34 @@ export default function EntityDetailPage() {

Scheduled Tasks

    {tasks.map((task) => ( -
  • -

    {task.name}

    -

    Schedule: {task.scheduleCron}

    -

    +

  • +
    +

    + {task.name} +

    + {!task.active && ( + + Inactive + + )} +
    +

    + Schedule: {task.scheduleCron} +

    +

    Email context: {getEmailLookbackLabel(task.emailLookback)}

    Details