From 4e5f32165c28a5ea8285c77c8ba4e8c75bb28921 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 06:31:37 -0500 Subject: [PATCH 1/5] [dev] [carhartlewis] lewis/comp-filter-evidence-tasks (#2106) * feat(tasks): add framework instances support to task filtering * feat(tasks): define FrameworkInstanceForTasks type for task components and added a handler for non-existent frameworks * feat(tasks): add validation for frameworkFilter in TaskList component --------- Co-authored-by: Lewis Carhart --- .../[orgId]/tasks/components/TaskList.tsx | 72 +++++++++++++++++-- .../tasks/components/TasksPageClient.tsx | 10 ++- apps/app/src/app/(app)/[orgId]/tasks/page.tsx | 42 +++++++++-- apps/app/src/app/(app)/[orgId]/tasks/types.ts | 10 +++ packages/docs/openapi.json | 2 - 5 files changed, 124 insertions(+), 12 deletions(-) create mode 100644 apps/app/src/app/(app)/[orgId]/tasks/types.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/components/TaskList.tsx b/apps/app/src/app/(app)/[orgId]/tasks/components/TaskList.tsx index 5ecd276b3..e1854f842 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/components/TaskList.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/components/TaskList.tsx @@ -28,6 +28,7 @@ import { Check, Circle, FolderTree, List, Search, XCircle } from 'lucide-react'; import { useParams } from 'next/navigation'; import { useQueryState } from 'nuqs'; import { useEffect, useMemo, useState } from 'react'; +import type { FrameworkInstanceForTasks } from '../types'; import { ModernTaskList } from './ModernTaskList'; import { TasksByCategory } from './TasksByCategory'; @@ -42,6 +43,7 @@ const statuses = [ export function TaskList({ tasks: initialTasks, members, + frameworkInstances, activeTab, }: { tasks: (Task & { @@ -61,6 +63,7 @@ export function TaskList({ }>; })[]; members: (Member & { user: User })[]; + frameworkInstances: FrameworkInstanceForTasks[]; activeTab: 'categories' | 'list'; }) { const params = useParams(); @@ -68,6 +71,7 @@ export function TaskList({ const [searchQuery, setSearchQuery] = useState(''); const [statusFilter, setStatusFilter] = useQueryState('status'); const [assigneeFilter, setAssigneeFilter] = useQueryState('assignee'); + const [frameworkFilter, setFrameworkFilter] = useQueryState('framework'); const [currentTab, setCurrentTab] = useState<'categories' | 'list'>(activeTab); // Sync activeTab prop with state when it changes @@ -75,6 +79,18 @@ export function TaskList({ setCurrentTab(activeTab); }, [activeTab]); + // Clear frameworkFilter when it's invalid or frameworks are empty. + // Prevents invisible filter (no dropdown when empty) and stale bookmarked URLs. + useEffect(() => { + if (!frameworkFilter) return; + const isValid = + frameworkInstances.length > 0 && + frameworkInstances.some((fw) => fw.id === frameworkFilter); + if (!isValid) { + setFrameworkFilter(null); + } + }, [frameworkFilter, frameworkInstances, setFrameworkFilter]); + const handleTabChange = async (value: string) => { const newTab = value as 'categories' | 'list'; setCurrentTab(newTab); @@ -100,7 +116,17 @@ export function TaskList({ }); }, [members]); - // Filter tasks by search query, status, and assignee + // Build a map of control IDs to their framework instances for efficient lookup + const frameworkControlIds = useMemo(() => { + const map = new Map>(); + for (const fw of frameworkInstances) { + const controlIds = new Set(fw.requirementsMapped.map((r) => r.controlId)); + map.set(fw.id, controlIds); + } + return map; + }, [frameworkInstances]); + + // Filter tasks by search query, status, assignee, and framework const filteredTasks = initialTasks.filter((task) => { const matchesSearch = searchQuery === '' || @@ -110,7 +136,16 @@ export function TaskList({ const matchesStatus = !statusFilter || task.status === statusFilter; const matchesAssignee = !assigneeFilter || task.assigneeId === assigneeFilter; - return matchesSearch && matchesStatus && matchesAssignee; + const matchesFramework = + !frameworkFilter || + (() => { + const fwControlIds = frameworkControlIds.get(frameworkFilter); + // Stale/invalid framework ID (e.g. from bookmarked URL): treat as "All frameworks" to match dropdown display + if (!fwControlIds) return true; + return task.controls.some((c) => fwControlIds.has(c.id)); + })(); + + return matchesSearch && matchesStatus && matchesAssignee && matchesFramework; }); // Calculate overall stats from all tasks (not filtered) @@ -571,6 +606,36 @@ export function TaskList({ + {frameworkInstances.length > 0 && ( + + )} + {/* Result Count */} - {(searchQuery || statusFilter || assigneeFilter) && ( + {(searchQuery || statusFilter || assigneeFilter || frameworkFilter) && (
{filteredTasks.length} {filteredTasks.length === 1 ? 'result' : 'results'}
@@ -664,7 +729,6 @@ export function TaskList({ - ); } diff --git a/apps/app/src/app/(app)/[orgId]/tasks/components/TasksPageClient.tsx b/apps/app/src/app/(app)/[orgId]/tasks/components/TasksPageClient.tsx index c7aec158b..ea350b004 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/components/TasksPageClient.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/components/TasksPageClient.tsx @@ -17,6 +17,7 @@ import { import { Add, ArrowDown } from '@trycompai/design-system/icons'; import { useState } from 'react'; import { toast } from 'sonner'; +import type { FrameworkInstanceForTasks } from '../types'; import { CreateTaskSheet } from './CreateTaskSheet'; import { TaskList } from './TaskList'; @@ -39,6 +40,7 @@ interface TasksPageClientProps { })[]; members: (Member & { user: User })[]; controls: { id: string; name: string }[]; + frameworkInstances: FrameworkInstanceForTasks[]; activeTab: 'categories' | 'list'; orgId: string; organizationName: string | null; @@ -49,6 +51,7 @@ export function TasksPageClient({ tasks, members, controls, + frameworkInstances, activeTab, orgId, organizationName, @@ -121,7 +124,12 @@ export function TasksPageClient({ } padding="default" > - + { const roles = parseRolesString(member?.role); const hasEvidenceExportAccess = - roles.includes(Role.auditor) || - roles.includes(Role.admin) || - roles.includes(Role.owner); + roles.includes(Role.auditor) || roles.includes(Role.admin) || roles.includes(Role.owner); return { hasEvidenceExportAccess, @@ -195,3 +194,36 @@ const getControls = async () => { return controls; }; + +const getFrameworkInstances = async () => { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + const orgId = session?.session.activeOrganizationId; + + if (!orgId) { + return []; + } + + const frameworkInstances = await db.frameworkInstance.findMany({ + where: { + organizationId: orgId, + }, + include: { + framework: { + select: { + id: true, + name: true, + }, + }, + requirementsMapped: { + select: { + controlId: true, + }, + }, + }, + }); + + return frameworkInstances; +}; diff --git a/apps/app/src/app/(app)/[orgId]/tasks/types.ts b/apps/app/src/app/(app)/[orgId]/tasks/types.ts new file mode 100644 index 000000000..9cbb311bd --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/tasks/types.ts @@ -0,0 +1,10 @@ +import type { FrameworkInstance } from '@db'; + +/** + * Shape of framework instance as returned by getFrameworkInstances and used by + * TaskList and TasksPageClient. Single source of truth to avoid type drift. + */ +export type FrameworkInstanceForTasks = Pick & { + framework: { id: string; name: string }; + requirementsMapped: { controlId: string }[]; +}; diff --git a/packages/docs/openapi.json b/packages/docs/openapi.json index b646c0a3a..fbab1e1eb 100644 --- a/packages/docs/openapi.json +++ b/packages/docs/openapi.json @@ -7024,7 +7024,6 @@ "enum": [ "todo", "in_progress", - "in_review", "done", "not_relevant", "failed" @@ -7333,7 +7332,6 @@ "enum": [ "todo", "in_progress", - "in_review", "done", "not_relevant", "failed" From d5d7ba1526accbf95dfc7efb7e9e84ee4953c460 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 08:01:38 -0500 Subject: [PATCH 2/5] [dev] [carhartlewis] lewis/comp-audit-handoff-information-fixes-cs-103 (#2111) * feat(context): resolve framework IDs to human-readable names in context entries * refactor(auditor): exclude framework selection and auditor sections from context --------- Co-authored-by: Lewis Carhart --- .../context-hub/data/getContextEntries.ts | 48 ++++++++++++++- .../actions/create-organization-minimal.ts | 11 +++- .../setup/actions/create-organization.ts | 9 ++- .../tasks/auditor/generate-auditor-content.ts | 58 +++++++++++++++---- 4 files changed, 111 insertions(+), 15 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/settings/context-hub/data/getContextEntries.ts b/apps/app/src/app/(app)/[orgId]/settings/context-hub/data/getContextEntries.ts index 85e429cd8..83cc74d6d 100644 --- a/apps/app/src/app/(app)/[orgId]/settings/context-hub/data/getContextEntries.ts +++ b/apps/app/src/app/(app)/[orgId]/settings/context-hub/data/getContextEntries.ts @@ -4,6 +4,48 @@ import { headers } from 'next/headers'; import { cache } from 'react'; import 'server-only'; +const FRAMEWORK_ID_PATTERN = /\bfrk_[a-z0-9]+\b/g; + +/** + * Detects framework IDs (frk_xxx) in context entry answers and replaces them + * with human-readable framework names. Handles legacy data from before the + * write-time fix was applied. + */ +async function resolveFrameworkIdsInEntries( + entries: T[], +): Promise { + // Collect all unique framework IDs across all entries + const allIds = new Set(); + for (const entry of entries) { + const matches = entry.answer.match(FRAMEWORK_ID_PATTERN); + if (matches) { + for (const id of matches) { + allIds.add(id); + } + } + } + + if (allIds.size === 0) return entries; + + // Batch-fetch framework names for all IDs + const frameworks = await db.frameworkEditorFramework.findMany({ + where: { id: { in: Array.from(allIds) } }, + select: { id: true, name: true }, + }); + + const idToName = new Map(frameworks.map((f) => [f.id, f.name])); + + // Replace IDs with names in each entry's answer + return entries.map((entry) => { + const resolvedAnswer = entry.answer.replace( + FRAMEWORK_ID_PATTERN, + (id) => idToName.get(id) ?? id, + ); + if (resolvedAnswer === entry.answer) return entry; + return { ...entry, answer: resolvedAnswer }; + }); +} + export const getContextEntries = cache( async ({ orgId, @@ -42,6 +84,10 @@ export const getContextEntries = cache( }); const total = await db.context.count({ where }); const pageCount = Math.ceil(total / perPage); - return { data: entries, pageCount }; + + // Resolve any legacy framework IDs to display names + const resolvedEntries = await resolveFrameworkIdsInEntries(entries); + + return { data: resolvedEntries, pageCount }; }, ); diff --git a/apps/app/src/app/(app)/setup/actions/create-organization-minimal.ts b/apps/app/src/app/(app)/setup/actions/create-organization-minimal.ts index 3854b6c12..3d82a3789 100644 --- a/apps/app/src/app/(app)/setup/actions/create-organization-minimal.ts +++ b/apps/app/src/app/(app)/setup/actions/create-organization-minimal.ts @@ -2,9 +2,9 @@ import { initializeOrganization } from '@/actions/organization/lib/initialize-organization'; import { authActionClientWithoutOrg } from '@/actions/safe-action'; +import { env } from '@/env.mjs'; import { createTrainingVideoEntries } from '@/lib/db/employee'; import { auth } from '@/utils/auth'; -import { env } from '@/env.mjs'; import { db } from '@db'; import { revalidatePath } from 'next/cache'; import { headers } from 'next/headers'; @@ -46,6 +46,13 @@ export const createOrganizationMinimal = authActionClientWithoutOrg // Check if self-hosted const isSelfHosted = env.NEXT_PUBLIC_SELF_HOSTED === 'true'; + // Resolve framework IDs to display names (e.g. "SOC 2", "ISO 27001") + const frameworks = await db.frameworkEditorFramework.findMany({ + where: { id: { in: parsedInput.frameworkIds } }, + select: { name: true }, + }); + const frameworkNames = frameworks.map((f) => f.name).join(', '); + // Create a new organization const newOrg = await db.organization.create({ data: { @@ -68,7 +75,7 @@ export const createOrganizationMinimal = authActionClientWithoutOrg context: { create: { question: 'Which compliance frameworks do you need?', - answer: parsedInput.frameworkIds.join(', '), + answer: frameworkNames || parsedInput.frameworkIds.join(', '), tags: ['onboarding'], }, }, diff --git a/apps/app/src/app/(app)/setup/actions/create-organization.ts b/apps/app/src/app/(app)/setup/actions/create-organization.ts index ab24c1f9b..0fc8d689c 100644 --- a/apps/app/src/app/(app)/setup/actions/create-organization.ts +++ b/apps/app/src/app/(app)/setup/actions/create-organization.ts @@ -41,6 +41,13 @@ export const createOrganization = authActionClientWithoutOrg // Create a new organization directly in the database const randomSuffix = Math.floor(100000 + Math.random() * 900000).toString(); + // Resolve framework IDs to display names (e.g. "SOC 2", "ISO 27001") + const frameworks = await db.frameworkEditorFramework.findMany({ + where: { id: { in: parsedInput.frameworkIds } }, + select: { name: true }, + }); + const frameworkNames = frameworks.map((f) => f.name).join(', '); + const newOrg = await db.organization.create({ data: { name: parsedInput.organizationName, @@ -62,7 +69,7 @@ export const createOrganization = authActionClientWithoutOrg question: step.question, answer: step.key === 'frameworkIds' - ? parsedInput.frameworkIds.join(', ') + ? frameworkNames || parsedInput.frameworkIds.join(', ') : (parsedInput[step.key as keyof typeof parsedInput] as string), tags: ['onboarding'], })), diff --git a/apps/app/src/trigger/tasks/auditor/generate-auditor-content.ts b/apps/app/src/trigger/tasks/auditor/generate-auditor-content.ts index 13372cf55..d35fdc365 100644 --- a/apps/app/src/trigger/tasks/auditor/generate-auditor-content.ts +++ b/apps/app/src/trigger/tasks/auditor/generate-auditor-content.ts @@ -1,5 +1,5 @@ import { getOrganizationContext } from '@/trigger/tasks/onboarding/onboard-organization-helpers'; -import { groq } from '@ai-sdk/groq'; +import { openai } from '@ai-sdk/openai'; import { db } from '@db'; import { logger, metadata, schemaTask } from '@trigger.dev/sdk'; import { generateText } from 'ai'; @@ -89,24 +89,54 @@ RULES: - No bullet points. ${TONE_RULES}`, - 'critical-vendors': `List vendors in this EXACT format, one per line: + 'critical-vendors': `Using the provided vendor/software list, narrow it down to ONLY the critical vendors from a SOC 2 perspective for the audit report. +A critical vendor is one that: +- Hosts or processes customer data (cloud infrastructure providers like AWS, GCP, Azure) +- Provides core identity / authentication services (e.g. Okta, Google Workspace, Microsoft 365 — but ONLY if used as the primary identity provider) +- Is essential to the company's production system or service delivery +- Handles sensitive data (e.g. payment processors IF the company processes payments as a core service) + +DO NOT INCLUDE vendors that are: +- Internal productivity / collaboration tools (e.g. Notion, Slack, Teams, Jira, Confluence, Asana) +- General business tools (e.g. Stripe, HubSpot, Intercom, Zendesk) +- HR / payroll tools (e.g. Rippling, Gusto, BambooHR) +- Marketing or analytics tools +- Version control or CI/CD tools (e.g. GitHub, GitLab) unless they host production infrastructure +- Security monitoring tools (e.g. Vanta, Drata, CrowdStrike) + +Typically a SOC 2 report includes only 3-6 critical vendors. Be very selective. + +FORMAT — one vendor per line: [Vendor Name] – [Type: SaaS/IaaS/PaaS] – ([Brief description of service]) EXAMPLE: -Zoom – SaaS – (Video conferencing / collaboration) AWS – IaaS / PaaS – (Cloud infrastructure and hosting) -Microsoft 365 – SaaS – (Office productivity and identity) +Google Workspace – SaaS – (Primary identity provider and email) +Datadog – SaaS – (Production monitoring and observability) RULES: - Do NOT include the section title. - Each vendor on its own line. - Follow the exact format: Name – Type – (Description) -- Only include vendors explicitly mentioned in sources. +- Only include vendors from the provided sources — do not add vendors not mentioned. +- Aim for 3-6 vendors maximum. ${TONE_RULES}`, - 'subservice-organizations': `List subservice organizations in this EXACT format: + 'subservice-organizations': `Identify the subservice organisations from a SOC 2 perspective. + +A subservice organisation is an external service provider whose infrastructure or platform the company DIRECTLY RELIES ON to deliver its own services to customers. In SOC 2 terms, these are typically the main cloud infrastructure / hosting providers (IaaS/PaaS) — e.g. AWS, Google Cloud Platform, Microsoft Azure. + +DO NOT INCLUDE: +- SaaS tools the company merely uses internally (e.g. Slack, Notion, Jira, GitHub, Stripe, HubSpot) +- Communication or collaboration platforms (e.g. Teams, Zoom) +- HR, payroll, or admin tools +- Security or monitoring tools +- Any tool that is NOT the primary infrastructure hosting the company's production system + +Typically there is only 1 (sometimes 2) subservice organisations. Be very selective. +FORMAT: Subservice organisations: [Name1], [Name2], ... If only one: "Subservice organisations: [Name]" @@ -118,7 +148,7 @@ RULES: - Do NOT include the section title. - Use "Subservice organisations:" prefix. - Just list the names, comma-separated if multiple. -- Only include organizations explicitly mentioned as subservice providers in sources. +- Look for where the company hosts its applications and data — that is the subservice organisation. ${TONE_RULES}`, }; @@ -145,7 +175,7 @@ async function scrapeWebsite(website: string): Promise { urls: [website], prompt: 'Extract all text content from this website, including company information, services, mission, vision, and any other relevant business information. Return the content as plain text or markdown.', - limit: 10 + limit: 10, }), }); @@ -223,7 +253,7 @@ async function generateSectionContent( contextHubText: string, ): Promise { const { text } = await generateText({ - model: groq('openai/gpt-oss-120b'), + model: openai('gpt-5.2'), system: `You are an expert at extracting and organizing company information for audit purposes. CRITICAL RULES: @@ -326,10 +356,16 @@ export const generateAuditorContentTask = schemaTask({ }; } - // Build context from organization data (excluding auditor sections to avoid circular reference) + // Build context from organization data, excluding: + // 1. Auditor sections (to avoid circular reference) + // 2. Framework selection (contains raw IDs like "frk_xxx" and isn't relevant to auditor content) const auditorQuestions = new Set(Object.values(SECTION_QUESTIONS)); + const excludedQuestions = new Set([ + ...auditorQuestions, + 'Which compliance frameworks do you need?', + ]); const contextHubText = questionsAndAnswers - .filter((qa) => !auditorQuestions.has(qa.question)) + .filter((qa) => !excludedQuestions.has(qa.question)) .map((qa) => `Q: ${qa.question}\nA: ${qa.answer}`) .join('\n\n'); From 38ac4f21e599b5f17961541db53d0922badd3a64 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 14:15:38 -0500 Subject: [PATCH 3/5] [dev] [carhartlewis] lewis/comp-join-date-calendar-cs-68 (#2112) * feat(context): resolve framework IDs to human-readable names in context entries * refactor(auditor): exclude framework selection and auditor sections from context * feat(people): enhance JoinDate component with date parsing and dropdown * refactor(people): simplify JoinDate component by removing date parsing logic * refactor(people): update label in JoinDate component to 'Join Date' * fix(people): add button type to Done button in JoinDate component --------- Co-authored-by: Lewis Carhart --- .../components/Fields/JoinDate.tsx | 91 +++++++++++-------- 1 file changed, 54 insertions(+), 37 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Fields/JoinDate.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Fields/JoinDate.tsx index 10b8ed210..da6f2c223 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Fields/JoinDate.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Fields/JoinDate.tsx @@ -1,10 +1,12 @@ +'use client'; + import { Button } from '@comp/ui/button'; -import { Calendar } from '@comp/ui/calendar'; -import { cn } from '@comp/ui/cn'; import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@comp/ui/form'; import { Popover, PopoverContent, PopoverTrigger } from '@comp/ui/popover'; +import { Calendar } from '@trycompai/design-system'; import { format } from 'date-fns'; -import { CalendarIcon } from 'lucide-react'; +import { ChevronDown } from 'lucide-react'; +import { useState } from 'react'; import type { Control } from 'react-hook-form'; import type { EmployeeFormValues } from '../EmployeeDetails'; @@ -15,45 +17,60 @@ export const JoinDate = ({ control: Control; disabled: boolean; }) => { + const [open, setOpen] = useState(false); + return ( ( - - - Join Date - - - - - - - - - date > new Date() // Explicitly type the date argument - } - initialFocus - /> - - - - - )} + Join Date + + + + + + date && field.onChange(date)} + captionLayout="dropdown" + disabled={(date) => date > new Date()} + /> +
+ +
+
+ + + + + ); + }} /> ); }; From 218b7b9a98e60ea5561ca6703a391355c5ce80dd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 00:24:55 -0500 Subject: [PATCH 4/5] [dev] [tofikwest] tofik/evidence-review-workflow (#2113) * feat(tasks): add email templates and notification logic for evidence review requests * fix(tasks): adjust layout of TabsContent in SingleTask component --------- Co-authored-by: Tofik Hasanov --- .../evidence-bulk-review-requested.tsx | 137 +++++ .../templates/evidence-review-requested.tsx | 119 +++++ apps/api/src/tasks/task-notifier.service.ts | 285 +++++++++++ apps/api/src/tasks/tasks.controller.ts | 195 +++++++- apps/api/src/tasks/tasks.service.ts | 452 ++++++++++++++++- ...e-organization-evidence-approval-action.ts | 46 ++ apps/app/src/actions/schema.ts | 4 + .../src/app/(app)/[orgId]/settings/page.tsx | 1 + .../tasks/[taskId]/components/SingleTask.tsx | 469 +++++++++++++----- .../[taskId]/components/TaskActivity.tsx | 258 ++++++++++ .../components/TaskAutomationStatusBadge.tsx | 2 +- .../components/TaskPropertiesSidebar.tsx | 138 +++++- .../tasks/[taskId]/hooks/use-task-activity.ts | 53 ++ .../app/(app)/[orgId]/tasks/[taskId]/page.tsx | 12 + .../components/BulkTaskStatusChangeModal.tsx | 135 +++-- .../components/ModernSingleStatusTaskList.tsx | 4 + .../tasks/components/ModernTaskList.tsx | 10 +- .../[orgId]/tasks/components/TaskCard.tsx | 2 +- .../[orgId]/tasks/components/TaskList.tsx | 37 +- .../tasks/components/TaskStatusIndicator.tsx | 5 +- .../tasks/components/TasksByCategory.tsx | 1 + .../tasks/components/TasksPageClient.tsx | 31 +- apps/app/src/app/(app)/[orgId]/tasks/page.tsx | 8 +- .../update-organization-evidence-approval.tsx | 78 +++ apps/app/src/components/status-indicator.tsx | 6 + .../migration.sql | 12 + .../migration.sql | 2 + packages/db/prisma/schema/auth.prisma | 1 + packages/db/prisma/schema/organization.prisma | 3 +- packages/db/prisma/schema/task.prisma | 7 + packages/docs/openapi.json | 293 ++++++++++- 31 files changed, 2602 insertions(+), 204 deletions(-) create mode 100644 apps/api/src/email/templates/evidence-bulk-review-requested.tsx create mode 100644 apps/api/src/email/templates/evidence-review-requested.tsx create mode 100644 apps/app/src/actions/organization/update-organization-evidence-approval-action.ts create mode 100644 apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/TaskActivity.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/tasks/[taskId]/hooks/use-task-activity.ts create mode 100644 apps/app/src/components/forms/organization/update-organization-evidence-approval.tsx create mode 100644 packages/db/prisma/migrations/20260206173918_add_evidence_approval/migration.sql create mode 100644 packages/db/prisma/migrations/20260206184014_add_task_previous_status/migration.sql diff --git a/apps/api/src/email/templates/evidence-bulk-review-requested.tsx b/apps/api/src/email/templates/evidence-bulk-review-requested.tsx new file mode 100644 index 000000000..ce37f025f --- /dev/null +++ b/apps/api/src/email/templates/evidence-bulk-review-requested.tsx @@ -0,0 +1,137 @@ +import * as React from 'react'; +import { + Body, + Button, + Container, + Font, + Heading, + Html, + Link, + Preview, + Section, + Tailwind, + Text, +} from '@react-email/components'; +import { Footer } from '../components/footer'; +import { Logo } from '../components/logo'; +import { getUnsubscribeUrl } from '@trycompai/email'; + +interface TaskItem { + title: string; + url: string; +} + +interface Props { + toName: string; + toEmail: string; + taskCount: number; + submittedByName: string; + organizationName: string; + tasksUrl: string; + tasks: TaskItem[]; +} + +export const EvidenceBulkReviewRequestedEmail = ({ + toName, + toEmail, + taskCount, + submittedByName, + organizationName, + tasksUrl, + tasks, +}: Props) => { + const unsubscribeUrl = getUnsubscribeUrl(toEmail); + const taskText = taskCount === 1 ? 'task' : 'tasks'; + + return ( + + + + + + + + {`${taskCount} ${taskText} submitted for your review`} + + + + + + + Evidence Review Requested + + + + Hello {toName}, + + + + {submittedByName} has submitted {taskCount}{' '} + {taskText} for your review in {organizationName}. + + + + Please review the evidence and approve or reject each task: + + +
+ {tasks.map((task, index) => ( + + {'• '} + + {task.title} + + + ))} +
+ +
+ +
+ + + or copy and paste this URL into your browser:{' '} + + {tasksUrl} + + + +
+ + Don't want to receive task assignment notifications?{' '} + + Manage your email preferences + + . + +
+ +
+ +