(null);
+
+ useEffect(() => {
+ if (isAddingCustom) inputRef.current?.focus();
+ }, [isAddingCustom]);
+
+ const handleSelectChange = (next: string | null) => {
+ if (next === ADD_CUSTOM_VALUE) {
+ setDraft('');
+ setIsAddingCustom(true);
+ return;
+ }
+ if (next && next !== value) onChange(next);
+ };
+
+ const handleSaveCustom = () => {
+ const trimmed = draft.trim();
+ if (
+ !trimmed ||
+ trimmed === ADD_CUSTOM_VALUE ||
+ trimmed.length > DEPARTMENT_MAX_LENGTH
+ )
+ return;
+ setSeen((prev) => {
+ if (prev.has(trimmed)) return prev;
+ const next = new Set(prev);
+ next.add(trimmed);
+ return next;
+ });
+ if (trimmed !== value) onChange(trimmed);
+ setIsAddingCustom(false);
+ setDraft('');
+ };
+
+ const handleCancelCustom = () => {
+ setIsAddingCustom(false);
+ setDraft('');
+ };
+
+ if (isAddingCustom) {
+ return (
+
+
+ setDraft(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ handleSaveCustom();
+ } else if (e.key === 'Escape') {
+ e.preventDefault();
+ handleCancelCustom();
+ }
+ }}
+ placeholder="Department name"
+ maxLength={DEPARTMENT_MAX_LENGTH}
+ disabled={disabled}
+ />
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/apps/app/src/components/forms/risks/create-risk-form.tsx b/apps/app/src/components/forms/risks/create-risk-form.tsx
index 58ec5b9085..32eabfc7ab 100644
--- a/apps/app/src/components/forms/risks/create-risk-form.tsx
+++ b/apps/app/src/components/forms/risks/create-risk-form.tsx
@@ -1,6 +1,7 @@
'use client';
import { createRiskSchema } from '@/actions/schema';
+import { DepartmentSelect } from '@/components/DepartmentSelect';
import { SelectAssignee } from '@/components/SelectAssignee';
import { useRiskActions } from '@/hooks/use-risks';
import { Button } from '@trycompai/ui/button';
@@ -130,18 +131,11 @@ export function CreateRisk({ assignees, onSuccess }: CreateRiskProps) {
render={({ field }) => (
Department
-
+
)}
diff --git a/apps/app/src/components/forms/risks/risk-overview.tsx b/apps/app/src/components/forms/risks/risk-overview.tsx
index 2656a636a0..b7a774be62 100644
--- a/apps/app/src/components/forms/risks/risk-overview.tsx
+++ b/apps/app/src/components/forms/risks/risk-overview.tsx
@@ -1,6 +1,7 @@
'use client';
import { updateRiskSchema } from '@/actions/schema';
+import { DepartmentSelect } from '@/components/DepartmentSelect';
import { SelectAssignee } from '@/components/SelectAssignee';
import { StatusIndicator } from '@/components/status-indicator';
import { usePermissions } from '@/hooks/use-permissions';
@@ -134,22 +135,11 @@ export function UpdateRiskOverview({
-
+ />
diff --git a/apps/app/src/hooks/use-risks.ts b/apps/app/src/hooks/use-risks.ts
index a893d5165f..dce9659941 100644
--- a/apps/app/src/hooks/use-risks.ts
+++ b/apps/app/src/hooks/use-risks.ts
@@ -6,7 +6,6 @@ import { ApiResponse } from '@/lib/api-client';
import { useCallback, useMemo } from 'react';
import type {
RiskCategory,
- Departments,
RiskStatus,
Likelihood,
Impact,
@@ -39,7 +38,7 @@ export interface Risk {
title: string;
description: string;
category: RiskCategory;
- department: Departments | null;
+ department: string | null;
status: RiskStatus;
likelihood: Likelihood;
impact: Impact;
@@ -83,7 +82,7 @@ interface CreateRiskData {
title: string;
description?: string;
category?: RiskCategory;
- department?: Departments;
+ department?: string;
status?: RiskStatus;
likelihood?: Likelihood;
impact?: Impact;
@@ -98,7 +97,7 @@ interface UpdateRiskData {
title?: string;
description?: string;
category?: RiskCategory;
- department?: Departments | null;
+ department?: string | null;
status?: RiskStatus;
likelihood?: Likelihood;
impact?: Impact;
diff --git a/apps/app/src/lib/embedding/index.ts b/apps/app/src/lib/embedding/index.ts
index 57c983815f..bcfe629c60 100644
--- a/apps/app/src/lib/embedding/index.ts
+++ b/apps/app/src/lib/embedding/index.ts
@@ -1,6 +1,5 @@
import 'server-only';
-import { Departments } from '@db';
import { Index } from '@upstash/vector';
import { openai } from '@ai-sdk/openai';
import { embedMany } from 'ai';
@@ -11,7 +10,7 @@ export type EntityKind = 'risk' | 'vendor' | 'task';
interface EntityInput {
id: string;
text: string;
- department?: Departments;
+ department?: string;
}
interface UpsertOptions {
@@ -47,7 +46,7 @@ interface FindSimilarTasksOptions {
export interface SimilarTaskResult {
id: string; // raw task id (sourceId), not the prefixed embedding id
score: number;
- department?: Departments;
+ department?: string;
}
// `text-embedding-3-large` truncated to 1536 dims via Matryoshka. The
@@ -92,7 +91,7 @@ export function computeEntityContentHash({
department,
}: {
text: string;
- department?: Departments;
+ department?: string;
}): string {
return createHash('sha256')
.update(`${EMBEDDING_MODEL}:${EMBEDDING_DIMENSIONS}:${department ?? ''}:${text}`)
@@ -190,7 +189,7 @@ export async function findSimilarTasks({
return {
id: meta.sourceId ?? String(r.id),
score: r.score,
- department: meta.department ? (meta.department as Departments) : undefined,
+ department: meta.department ?? undefined,
};
});
}
diff --git a/apps/app/src/lib/link-suggestions.ts b/apps/app/src/lib/link-suggestions.ts
index 693affa834..a1ef12895e 100644
--- a/apps/app/src/lib/link-suggestions.ts
+++ b/apps/app/src/lib/link-suggestions.ts
@@ -1,5 +1,3 @@
-import { Departments } from '@db';
-
const DEFAULT_THRESHOLD = 0.65;
const DEFAULT_TOP_K = 5;
const DEFAULT_DEPARTMENT_BOOST = 0.05;
@@ -7,11 +5,11 @@ const DEFAULT_DEPARTMENT_BOOST = 0.05;
export interface Candidate {
id: string;
score: number; // raw cosine similarity from the vector store
- department?: Departments;
+ department?: string;
}
export interface LinkSuggestionsOptions {
- source: { department?: Departments };
+ source: { department?: string };
candidates: Candidate[];
threshold?: number;
topK?: number;
@@ -39,10 +37,10 @@ export function linkSuggestions({
if (candidates.length === 0) return [];
const sourceDept = source.department;
- const shouldBoost = (candidateDept?: Departments) =>
+ const shouldBoost = (candidateDept?: string) =>
sourceDept !== undefined &&
candidateDept !== undefined &&
- candidateDept !== Departments.none &&
+ candidateDept !== 'none' &&
sourceDept === candidateDept;
const boosted: LinkSuggestion[] = candidates.map((c) => ({
diff --git a/apps/app/src/trigger/tasks/task/policy-acknowledgment-digest-helpers.ts b/apps/app/src/trigger/tasks/task/policy-acknowledgment-digest-helpers.ts
index bc738efc26..da26088017 100644
--- a/apps/app/src/trigger/tasks/task/policy-acknowledgment-digest-helpers.ts
+++ b/apps/app/src/trigger/tasks/task/policy-acknowledgment-digest-helpers.ts
@@ -2,7 +2,7 @@
* Helper types and pure filter function for the policy acknowledgment digest.
* Extracted from the scheduled task for testability.
*/
-import type { Departments, PolicyVisibility } from '@db';
+import type { PolicyVisibility } from '@db';
// Inlined from @trycompai/auth to avoid pulling that package into the Trigger.dev bundle.
// Keep in sync with packages/auth/src/permissions.ts BUILT_IN_ROLE_OBLIGATIONS.
@@ -93,13 +93,13 @@ export interface DigestPolicy {
name: string;
signedBy: string[];
visibility: PolicyVisibility;
- visibleToDepartments: Departments[];
+ visibleToDepartments: string[];
}
export interface DigestMember {
id: string;
role: string;
- department: Departments | null;
+ department: string | null;
user: { id: string; name: string | null; email: string; role?: string | null };
}
diff --git a/packages/db/prisma/migrations/20260602120000_policy_department_to_string/migration.sql b/packages/db/prisma/migrations/20260602120000_policy_department_to_string/migration.sql
new file mode 100644
index 0000000000..a84f3c456a
--- /dev/null
+++ b/packages/db/prisma/migrations/20260602120000_policy_department_to_string/migration.sql
@@ -0,0 +1,7 @@
+-- AlterTable
+ALTER TABLE "Policy" ALTER COLUMN "department" TYPE TEXT USING "department"::TEXT;
+
+-- AlterTable
+ALTER TABLE "Policy" ALTER COLUMN "visibleToDepartments" DROP DEFAULT;
+ALTER TABLE "Policy" ALTER COLUMN "visibleToDepartments" TYPE TEXT[] USING "visibleToDepartments"::TEXT[];
+ALTER TABLE "Policy" ALTER COLUMN "visibleToDepartments" SET DEFAULT ARRAY[]::TEXT[];
diff --git a/packages/db/prisma/migrations/20260602130000_member_risk_task_department_to_string/migration.sql b/packages/db/prisma/migrations/20260602130000_member_risk_task_department_to_string/migration.sql
new file mode 100644
index 0000000000..3b9e6b145f
--- /dev/null
+++ b/packages/db/prisma/migrations/20260602130000_member_risk_task_department_to_string/migration.sql
@@ -0,0 +1,12 @@
+-- AlterTable: Member.department — convert enum column to TEXT, preserve "none" default
+ALTER TABLE "Member" ALTER COLUMN "department" DROP DEFAULT;
+ALTER TABLE "Member" ALTER COLUMN "department" TYPE TEXT USING "department"::TEXT;
+ALTER TABLE "Member" ALTER COLUMN "department" SET DEFAULT 'none';
+
+-- AlterTable: Risk.department — convert nullable enum column to TEXT (no default)
+ALTER TABLE "Risk" ALTER COLUMN "department" TYPE TEXT USING "department"::TEXT;
+
+-- AlterTable: Task.department — convert nullable enum column to TEXT, preserve "none" default
+ALTER TABLE "Task" ALTER COLUMN "department" DROP DEFAULT;
+ALTER TABLE "Task" ALTER COLUMN "department" TYPE TEXT USING "department"::TEXT;
+ALTER TABLE "Task" ALTER COLUMN "department" SET DEFAULT 'none';
diff --git a/packages/db/prisma/schema/auth.prisma b/packages/db/prisma/schema/auth.prisma
index 6c9c12383c..8eb3abab39 100644
--- a/packages/db/prisma/schema/auth.prisma
+++ b/packages/db/prisma/schema/auth.prisma
@@ -200,7 +200,7 @@ model Member {
role String // Purposefully a string, since BetterAuth doesn't support enums this way
createdAt DateTime @default(now())
- department Departments @default(none)
+ department String @default("none")
jobTitle String?
isActive Boolean @default(true)
deactivated Boolean @default(false)
diff --git a/packages/db/prisma/schema/policy.prisma b/packages/db/prisma/schema/policy.prisma
index 50f1c87853..2cf80a4373 100644
--- a/packages/db/prisma/schema/policy.prisma
+++ b/packages/db/prisma/schema/policy.prisma
@@ -16,7 +16,7 @@ model Policy {
content Json[]
draftContent Json[] @default([])
frequency Frequency?
- department Departments?
+ department String?
isRequiredToSign Boolean @default(true)
signedBy String[] @default([])
reviewDate DateTime?
@@ -26,7 +26,7 @@ model Policy {
// Visibility settings (for department-specific policies)
visibility PolicyVisibility @default(ALL)
- visibleToDepartments Departments[] @default([])
+ visibleToDepartments String[] @default([])
// Dates
createdAt DateTime @default(now())
diff --git a/packages/db/prisma/schema/risk.prisma b/packages/db/prisma/schema/risk.prisma
index b4a95441dd..76ae882a47 100644
--- a/packages/db/prisma/schema/risk.prisma
+++ b/packages/db/prisma/schema/risk.prisma
@@ -4,7 +4,7 @@ model Risk {
title String
description String
category RiskCategory
- department Departments?
+ department String?
status RiskStatus @default(open)
likelihood Likelihood @default(very_unlikely)
impact Impact @default(insignificant)
diff --git a/packages/db/prisma/schema/task.prisma b/packages/db/prisma/schema/task.prisma
index 3b054a0597..37b0c9925c 100644
--- a/packages/db/prisma/schema/task.prisma
+++ b/packages/db/prisma/schema/task.prisma
@@ -8,7 +8,7 @@ model Task {
frequency TaskFrequency?
integrationScheduleFrequency TaskFrequency @default(daily)
integrationLastRunAt DateTime?
- department Departments? @default(none)
+ department String? @default("none")
order Int @default(0)
// Dates