+ Archived on{" "}
+ {archivedDate.toLocaleDateString("en-US", {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ })}
+
+ );
+ }
+
return (
{date.toLocaleDateString("en-US", {
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/components/table/components/filterCategories.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/components/table/components/filterCategories.tsx
index ae8855fb53..a539f18d11 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/components/table/components/filterCategories.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/components/table/components/filterCategories.tsx
@@ -1,6 +1,6 @@
"use client";
-import { STATUS_FILTERS } from "./filterConfigs";
+import { ARCHIVED_FILTER, STATUS_FILTERS } from "./filterConfigs";
import type { User } from "next-auth";
interface FilterCategoriesProps {
@@ -8,6 +8,8 @@ interface FilterCategoriesProps {
setStatus: (status: string | null) => void;
ownerId: string | null;
setOwnerId: (ownerId: string | null) => void;
+ isArchived: string | null;
+ setIsArchived: (isArchived: string | null) => void;
users: User[];
setPage: (page: string) => void;
}
@@ -17,6 +19,8 @@ export function getFilterCategories({
setStatus,
ownerId,
setOwnerId,
+ isArchived,
+ setIsArchived,
users,
setPage,
}: FilterCategoriesProps) {
@@ -32,6 +36,19 @@ export function getFilterCategories({
},
})),
},
+ {
+ label: "Filter by Archived",
+ items: [
+ {
+ ...ARCHIVED_FILTER,
+ checked: isArchived === ARCHIVED_FILTER.value,
+ onChange: (checked: boolean) => {
+ setIsArchived(checked ? ARCHIVED_FILTER.value : null);
+ setPage("1");
+ },
+ },
+ ],
+ },
{
label: "Filter by Owner",
items: users.map((user) => ({
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/components/table/components/filterConfigs.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/components/table/components/filterConfigs.tsx
index 7d1a7d631d..244d74ea58 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/components/table/components/filterConfigs.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/components/table/components/filterConfigs.tsx
@@ -16,9 +16,10 @@ export const STATUS_FILTERS = [
value: "needs_review",
icon:
,
},
- {
- label: "Archived",
- value: "archived",
- icon:
,
- },
] as const;
+
+export const ARCHIVED_FILTER = {
+ label: "Archived",
+ value: "true",
+ icon:
,
+} as const;
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/components/table/hooks/usePoliciesTableContext.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/components/table/hooks/usePoliciesTableContext.tsx
index bcdfc1e17e..f93cc63379 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/components/table/hooks/usePoliciesTableContext.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/components/table/hooks/usePoliciesTableContext.tsx
@@ -17,6 +17,7 @@ interface PoliciesTableContextType {
search: string;
status: string | null;
ownerId: string | null;
+ isArchived: string | null;
page: string;
pageSize: string;
@@ -24,6 +25,7 @@ interface PoliciesTableContextType {
setSearch: (value: string) => void;
setStatus: (value: string | null) => void;
setOwnerId: (value: string | null) => void;
+ setIsArchived: (value: string | null) => void;
setPage: (value: string) => void;
setPageSize: (value: string) => void;
@@ -51,6 +53,7 @@ export function PoliciesTableProvider({ children }: { children: ReactNode }) {
// Query state for other filters
const [status, setStatus] = useQueryState("status");
const [ownerId, setOwnerId] = useQueryState("ownerId");
+ const [isArchived, setIsArchived] = useQueryState("isArchived");
const [page, setPage] = useQueryState("page", { defaultValue: "1" });
const [pageSize, setPageSize] = useQueryState("pageSize", {
defaultValue: "10",
@@ -68,6 +71,7 @@ export function PoliciesTableProvider({ children }: { children: ReactNode }) {
search,
status: status || undefined,
ownerId: ownerId || undefined,
+ isArchived: isArchived || undefined,
page: currentPage,
pageSize: currentPageSize,
});
@@ -77,7 +81,7 @@ export function PoliciesTableProvider({ children }: { children: ReactNode }) {
if (initialLoadCompleted.current) {
setIsSearching(true);
}
- }, [search, status, ownerId, page, pageSize]);
+ }, [search, status, ownerId, isArchived, page, pageSize]);
// Track when loading changes
useEffect(() => {
@@ -103,13 +107,14 @@ export function PoliciesTableProvider({ children }: { children: ReactNode }) {
// Check if any filters are active
const hasActiveFilters = useMemo(() => {
- return status !== null || ownerId !== null;
- }, [status, ownerId]);
+ return status !== null || ownerId !== null || isArchived !== null;
+ }, [status, ownerId, isArchived]);
// Clear all filters
const clearFilters = () => {
setStatus(null);
setOwnerId(null);
+ setIsArchived(null);
setPage("1"); // Reset to first page when clearing filters
setSearch(""); // Clear search
};
@@ -119,6 +124,7 @@ export function PoliciesTableProvider({ children }: { children: ReactNode }) {
search,
status,
ownerId,
+ isArchived,
page,
pageSize,
@@ -126,6 +132,7 @@ export function PoliciesTableProvider({ children }: { children: ReactNode }) {
setSearch,
setStatus,
setOwnerId,
+ setIsArchived,
setPage,
setPageSize,
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/hooks/usePolicies.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/hooks/usePolicies.ts
index 4708df8ab7..44b46ca9f4 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/hooks/usePolicies.ts
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/hooks/usePolicies.ts
@@ -30,6 +30,7 @@ export function usePolicies({
search = "",
status = "",
ownerId = "",
+ isArchived = "",
sort = "",
page = 1,
pageSize = 10,
@@ -40,8 +41,17 @@ export function usePolicies({
isLoading,
mutate: revalidatePolicies,
} = useSWR
(
- ["policies", { search, status, page, pageSize, ownerId, sort }],
- () => fetchPolicies({ search, status, page, pageSize, ownerId, sort }),
+ ["policies", { search, status, page, pageSize, ownerId, isArchived, sort }],
+ () =>
+ fetchPolicies({
+ search,
+ status,
+ page,
+ pageSize,
+ ownerId,
+ isArchived,
+ sort,
+ }),
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/hooks/usePoliciesTableContext.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/hooks/usePoliciesTableContext.tsx
new file mode 100644
index 0000000000..c05e7edeab
--- /dev/null
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/hooks/usePoliciesTableContext.tsx
@@ -0,0 +1,169 @@
+"use client";
+
+import {
+ createContext,
+ useContext,
+ useMemo,
+ useState,
+ useRef,
+ useEffect,
+ type ReactNode,
+} from "react";
+import { useQueryState } from "nuqs";
+import { usePolicies } from "./usePolicies";
+
+interface PoliciesTableContextType {
+ // State
+ search: string;
+ status: string | null;
+ ownerId: string | null;
+ isArchived: string | null;
+ page: string;
+ pageSize: string;
+
+ // Setters
+ setSearch: (value: string) => void;
+ setStatus: (value: string | null) => void;
+ setOwnerId: (value: string | null) => void;
+ setIsArchived: (value: string | null) => void;
+ setPage: (value: string) => void;
+ setPageSize: (value: string) => void;
+
+ // Data
+ policies: any[] | undefined;
+ total: number | undefined;
+ isLoading: boolean;
+ isSearching: boolean;
+
+ // Derived data
+ hasActiveFilters: boolean;
+
+ // Actions
+ clearFilters: () => void;
+}
+
+const PoliciesTableContext = createContext<
+ PoliciesTableContextType | undefined
+>(undefined);
+
+export function PoliciesTableProvider({ children }: { children: ReactNode }) {
+ // Local state for search
+ const [search, setSearch] = useState("");
+
+ // Query state for other filters
+ const [status, setStatus] = useQueryState("status");
+ const [ownerId, setOwnerId] = useQueryState("ownerId");
+ const [isArchived, setIsArchived] = useQueryState("isArchived");
+ const [page, setPage] = useQueryState("page", { defaultValue: "1" });
+ const [pageSize, setPageSize] = useQueryState("pageSize", {
+ defaultValue: "10",
+ });
+
+ // Track if this is initial load or a search/filter update
+ const initialLoadCompleted = useRef(false);
+ const [isSearching, setIsSearching] = useState(false);
+
+ const currentPage = Number.parseInt(page, 10);
+ const currentPageSize = Number.parseInt(pageSize, 10);
+
+ // Fetch data
+ const { policies, total, isLoading } = usePolicies({
+ search,
+ status: status || undefined,
+ ownerId: ownerId || undefined,
+ isArchived: isArchived || undefined,
+ page: currentPage,
+ pageSize: currentPageSize,
+ });
+
+ // Track when search params change
+ useEffect(() => {
+ if (initialLoadCompleted.current) {
+ setIsSearching(true);
+ }
+ }, [search, status, ownerId, isArchived, page, pageSize]);
+
+ // Track when loading changes
+ useEffect(() => {
+ if (isLoading === false) {
+ // Small delay to ensure UI transitions properly
+ setTimeout(() => {
+ initialLoadCompleted.current = true;
+ setIsSearching(false);
+ }, 50);
+ }
+ }, [isLoading]);
+
+ // Additional safety reset for isSearching when data changes
+ useEffect(() => {
+ if (policies && isSearching) {
+ // If we have data, ensure isSearching is eventually set to false
+ const timer = setTimeout(() => {
+ setIsSearching(false);
+ }, 100);
+ return () => clearTimeout(timer);
+ }
+ }, [policies, isSearching]);
+
+ // Check if any filters are active
+ const hasActiveFilters = useMemo(() => {
+ return status !== null || ownerId !== null || isArchived !== null;
+ }, [status, ownerId, isArchived]);
+
+ // Clear all filters
+ const clearFilters = () => {
+ setStatus(null);
+ setOwnerId(null);
+ setIsArchived(null);
+ setPage("1"); // Reset to first page when clearing filters
+ setSearch(""); // Clear search
+ };
+
+ const contextValue: PoliciesTableContextType = {
+ // State
+ search,
+ status,
+ ownerId,
+ isArchived,
+ page,
+ pageSize,
+
+ // Setters
+ setSearch,
+ setStatus,
+ setOwnerId,
+ setIsArchived,
+ setPage,
+ setPageSize,
+
+ // Data
+ policies,
+ total, // Use the total directly from the API
+ isLoading,
+ isSearching,
+
+ // Derived data
+ hasActiveFilters,
+
+ // Actions
+ clearFilters,
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function usePoliciesTable() {
+ const context = useContext(PoliciesTableContext);
+
+ if (context === undefined) {
+ throw new Error(
+ "usePoliciesTable must be used within a PoliciesTableProvider",
+ );
+ }
+
+ return context;
+}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/types/index.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/types/index.ts
index 39b43ae651..6c95a60c66 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/types/index.ts
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/types/index.ts
@@ -11,12 +11,14 @@ export const policySchema = z.object({
description: z.string().nullable(),
slug: z.string(),
}),
+ isArchived: z.boolean().optional(),
});
export const policiesInputSchema = z.object({
search: z.string().optional(),
status: z.string().optional(),
ownerId: z.string().optional(),
+ isArchived: z.string().optional(),
sort: z.string().optional(),
page: z.number().default(1),
pageSize: z.number().default(10),
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/editor/actions/update-policy.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/editor/actions/update-policy.ts
index 9d1d23d780..75f7a7e62b 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/editor/actions/update-policy.ts
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/editor/actions/update-policy.ts
@@ -7,98 +7,101 @@ import { db } from "@bubba/db";
import { appErrors, updatePolicySchema } from "../types";
export const updatePolicy = authActionClient
- .schema(updatePolicySchema)
- .metadata({
- name: "update-policy",
- track: {
- event: "update-policy",
- channel: "server",
- },
- })
- .action(async ({ parsedInput }): Promise => {
- const { policyId, content, status } = parsedInput;
+ .schema(updatePolicySchema)
+ .metadata({
+ name: "update-policy",
+ track: {
+ event: "update-policy",
+ channel: "server",
+ },
+ })
+ .action(async ({ parsedInput }): Promise => {
+ const { policyId, content, status } = parsedInput;
- const session = await auth();
- const organizationId = session?.user.organizationId;
+ const session = await auth();
+ const organizationId = session?.user.organizationId;
- if (!organizationId) {
- return {
- success: false,
- error: appErrors.UNAUTHORIZED.message,
- };
- }
+ if (!organizationId) {
+ return {
+ success: false,
+ error: appErrors.UNAUTHORIZED.message,
+ };
+ }
- try {
- const existingPolicy = await db.organizationPolicy.findUnique({
- where: {
- id: policyId,
- organizationId,
- },
- });
+ try {
+ const existingPolicy = await db.organizationPolicy.findUnique({
+ where: {
+ id: policyId,
+ organizationId,
+ },
+ });
- if (!existingPolicy) {
- return {
- success: false,
- error: appErrors.NOT_FOUND.message,
- };
- }
+ if (!existingPolicy) {
+ return {
+ success: false,
+ error: appErrors.NOT_FOUND.message,
+ };
+ }
- const updateData: Record = {};
+ const updateData: Record = {};
- if (content !== undefined) {
- console.log("CONTENT TYPE:", typeof content);
+ if (content !== undefined) {
+ console.log("CONTENT TYPE:", typeof content);
- if (typeof content === "object" && content !== null) {
- if (
- "type" in content &&
- content.type === "doc" &&
- "content" in content &&
- Array.isArray(content.content)
- ) {
- updateData.content = content.content;
- console.log("Extracted content array from TipTap doc");
- } else if (Array.isArray(content)) {
- updateData.content = content;
- } else {
- console.log("Unknown content format - using as is");
- updateData.content = content;
- }
- } else {
- updateData.content = content;
- }
- }
+ if (typeof content === "object" && content !== null) {
+ if (
+ "type" in content &&
+ content.type === "doc" &&
+ "content" in content &&
+ Array.isArray(content.content)
+ ) {
+ updateData.content = content.content;
+ console.log("Extracted content array from TipTap doc");
+ } else if (Array.isArray(content)) {
+ updateData.content = content;
+ } else {
+ console.log("Unknown content format - using as is");
+ updateData.content = content;
+ }
+ } else {
+ updateData.content = content;
+ }
+ }
- if (status) {
- updateData.status = status;
- }
+ if (status) {
+ updateData.status = status;
+ }
- if (Object.keys(updateData).length === 0) {
- return {
- success: true,
- data: { id: policyId, status: existingPolicy.status },
- };
- }
+ if (Object.keys(updateData).length === 0) {
+ return {
+ success: true,
+ data: { id: policyId, status: existingPolicy.status },
+ };
+ }
- const updatedPolicy = await db.organizationPolicy.update({
- where: {
- id: policyId,
- },
- data: updateData,
- select: {
- id: true,
- status: true,
- },
- });
+ const updatedPolicy = await db.organizationPolicy.update({
+ where: {
+ id: policyId,
+ },
+ data: {
+ ...updateData,
+ signedBy: [],
+ },
+ select: {
+ id: true,
+ status: true,
+ },
+ });
- return {
- success: true,
- data: updatedPolicy,
- };
- } catch (error) {
- console.error("Error updating policy:", error);
- return {
- success: false,
- error: appErrors.UNEXPECTED_ERROR.message,
- };
- }
- });
+ return {
+ success: true,
+ data: updatedPolicy,
+ };
+ } catch (error) {
+ console.error("Error updating policy:", error);
+ return {
+ success: false,
+ error: appErrors.UNEXPECTED_ERROR.message,
+ };
+ }
+ });
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/update-vendor-inherent-risk.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/update-vendor-inherent-risk.ts
index 5af380f413..99aa857fbd 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/update-vendor-inherent-risk.ts
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/update-vendor-inherent-risk.ts
@@ -1,6 +1,6 @@
"use server";
-import { db } from "@/db";
+import { db } from "@bubba/db";
import { appErrors } from "@/lib/errors";
import type { ActionResponse } from "@/types/actions";
import { createSafeActionClient } from "next-safe-action";
@@ -21,13 +21,13 @@ export const updateVendorInherentRisk = createSafeActionClient()
});
revalidatePath(`/vendors/${parsedInput.vendorId}`);
-
+
return { success: true };
} catch (error) {
return {
success: false,
- error: error instanceof Error ? error.message : appErrors.UNEXPECTED_ERROR,
+ error:
+ error instanceof Error ? error.message : appErrors.UNEXPECTED_ERROR,
};
}
-
- });
\ No newline at end of file
+ });
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/update-vendor-residual-risk.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/update-vendor-residual-risk.ts
index 64a56d730e..913e8a5882 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/update-vendor-residual-risk.ts
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/update-vendor-residual-risk.ts
@@ -4,7 +4,7 @@ import { createSafeActionClient } from "next-safe-action";
import { z } from "zod";
import type { ActionResponse } from "@/types/actions";
import { appErrors } from "@/lib/errors";
-import { db } from "@/db";
+import { db } from "@bubba/db";
import type { VendorResidualRisk } from "@prisma/client";
const schema = z.object({
@@ -25,7 +25,8 @@ export const updateVendorResidualRisk = createSafeActionClient()
} catch (error) {
return {
success: false,
- error: error instanceof Error ? error.message : appErrors.UNEXPECTED_ERROR,
+ error:
+ error instanceof Error ? error.message : appErrors.UNEXPECTED_ERROR,
};
}
- });
\ No newline at end of file
+ });
diff --git a/apps/app/src/components/forms/policies/policy-overview.tsx b/apps/app/src/components/forms/policies/policy-overview.tsx
index 081fdb12b9..309c687778 100644
--- a/apps/app/src/components/forms/policies/policy-overview.tsx
+++ b/apps/app/src/components/forms/policies/policy-overview.tsx
@@ -23,6 +23,7 @@ import {
FormItem,
FormLabel,
FormMessage,
+ FormDescription,
} from "@bubba/ui/form";
import { Popover, PopoverContent, PopoverTrigger } from "@bubba/ui/popover";
import {
@@ -40,11 +41,11 @@ import { useAction } from "next-safe-action/hooks";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import type { z } from "zod";
+import { Switch } from "@bubba/ui/switch";
const policyStatuses: PolicyStatus[] = [
"draft",
"published",
- "archived",
"needs_review",
] as const;
@@ -85,7 +86,7 @@ export function UpdatePolicyOverview({
department: organizationPolicy.department ?? Departments.admin,
review_frequency: organizationPolicy.frequency ?? Frequency.monthly,
review_date: reviewDate,
- isRequiredToSign: (organizationPolicy.policy as any).isRequiredToSign
+ isRequiredToSign: organizationPolicy.isRequiredToSign
? "required"
: "not_required",
},
@@ -289,29 +290,19 @@ export function UpdatePolicyOverview({
control={form.control}
name="isRequiredToSign"
render={({ field }) => (
-
+
{t("policies.overview.form.signature_requirement")}
-
+ {
+ field.onChange(checked ? "required" : "not_required");
+ }}
+ />
+
)}
/>
diff --git a/apps/app/src/components/header.tsx b/apps/app/src/components/header.tsx
index 25abf862f8..58a3c844f6 100644
--- a/apps/app/src/components/header.tsx
+++ b/apps/app/src/components/header.tsx
@@ -48,7 +48,7 @@ export async function Header() {
-