diff --git a/frontend/src/__tests__/pages/DashboardPage.test.tsx b/frontend/src/__tests__/pages/DashboardPage.test.tsx index e925511..f2a9d09 100644 --- a/frontend/src/__tests__/pages/DashboardPage.test.tsx +++ b/frontend/src/__tests__/pages/DashboardPage.test.tsx @@ -64,8 +64,8 @@ describe('DashboardPage', () => { render(, { wrapper }) await waitFor(() => { - expect(screen.getByText(/Scheduled Tasks/i)).toBeInTheDocument() - expect(screen.getByText(/Entity A/i)).toBeInTheDocument() + expect(screen.getByRole('heading', { name: /Scheduled Tasks/i })).toBeInTheDocument() + expect(screen.getByText(/Entity A default task/i)).toBeInTheDocument() expect(screen.getByText(/0 9 \* \* 1/i)).toBeInTheDocument() }) }) diff --git a/frontend/src/__tests__/pages/EntityDetailPage.test.tsx b/frontend/src/__tests__/pages/EntityDetailPage.test.tsx index 6a0b0ec..54ba48f 100644 --- a/frontend/src/__tests__/pages/EntityDetailPage.test.tsx +++ b/frontend/src/__tests__/pages/EntityDetailPage.test.tsx @@ -21,6 +21,8 @@ const wrapper = ({ children }: { children: React.ReactNode }) => ( describe('EntityDetailPage', () => { it('should_renderEntityAndTasks_when_pageLoads', async () => { + vi.mocked(tasksApi.getEmailLookbackLabel).mockReturnValue('Last week') + vi.mocked(entitiesApi.getEntity).mockResolvedValue({ id: 'entity-1', name: 'Entity A', @@ -54,6 +56,8 @@ describe('EntityDetailPage', () => { }) it('should_generatePreviewAndCreateTask_when_formSubmitted', async () => { + vi.mocked(tasksApi.getEmailLookbackLabel).mockReturnValue('Last week') + vi.mocked(entitiesApi.getEntity).mockResolvedValue({ id: 'entity-1', name: 'Entity A', @@ -89,7 +93,8 @@ describe('EntityDetailPage', () => { fireEvent.click(screen.getByRole('button', { name: /generate test message/i })) await waitFor(() => { - expect(tasksApi.generateTaskPreview).toHaveBeenCalledWith( + expect(tasksApi.generateTaskPreview).toHaveBeenCalled() + expect(vi.mocked(tasksApi.generateTaskPreview).mock.calls[0][0]).toEqual( expect.objectContaining({ entityId: 'entity-1', name: 'Morning Blast', @@ -104,7 +109,8 @@ describe('EntityDetailPage', () => { fireEvent.click(screen.getByRole('button', { name: /create task/i })) await waitFor(() => { - expect(tasksApi.createTask).toHaveBeenCalledWith( + expect(tasksApi.createTask).toHaveBeenCalled() + expect(vi.mocked(tasksApi.createTask).mock.calls[0][0]).toEqual( expect.objectContaining({ entityId: 'entity-1', name: 'Morning Blast', diff --git a/frontend/src/api/tasksApi.ts b/frontend/src/api/tasksApi.ts new file mode 100644 index 0000000..ced8104 --- /dev/null +++ b/frontend/src/api/tasksApi.ts @@ -0,0 +1,82 @@ +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 + createdAt: string +} + +export interface EntityTaskCreateDto { + entityId: string + name: string + prompt: string + scheduleCron: string + emailLookback: EmailLookback +} + +function readTasks(): EntityTaskResponse[] { + const raw = localStorage.getItem(STORAGE_KEY) + if (!raw) return [] + + try { + return JSON.parse(raw) as EntityTaskResponse[] + } 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() +} + +/** Returns scheduled tasks for a specific entity. */ +export async function getTasksByEntity(entityId: string): Promise { + return readTasks().filter((task) => task.entityId === entityId) +} + +/** Creates a scheduled task in local storage. */ +export async function createTask(data: EntityTaskCreateDto): Promise { + const current = readTasks() + const task: EntityTaskResponse = { + ...data, + id: crypto.randomUUID(), + createdAt: new Date().toISOString(), + } + + current.push(task) + writeTasks(current) + return task +} diff --git a/frontend/src/components/NavBar.tsx b/frontend/src/components/NavBar.tsx index de32649..c7ec331 100644 --- a/frontend/src/components/NavBar.tsx +++ b/frontend/src/components/NavBar.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from 'react' import { Link, useLocation } from 'react-router-dom' const NAV_LINKS = [ @@ -9,20 +10,41 @@ const NAV_LINKS = [ /** Top navigation bar for authenticated pages. */ export default function NavBar() { const { pathname } = useLocation() + const [theme, setTheme] = useState<'dark' | 'light'>(() => { + return document.documentElement.classList.contains('dark') ? 'dark' : 'light' + }) + + useEffect(() => { + if (theme === 'dark') { + document.documentElement.classList.add('dark') + localStorage.setItem('theme', 'dark') + return + } + + document.documentElement.classList.remove('dark') + localStorage.setItem('theme', 'light') + }, [theme]) return ( -