From 0e0539f1f39a83cfabd462e95f5f837d507a38af Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Tue, 17 Mar 2026 00:38:44 +0100 Subject: [PATCH] Replace console frontend with unified findings pages Add FindingsPage, FindingDetailsPage, and CreateFindingDialog supporting all finding kinds (nonconformity, observation, exception) with filtering, sorting, and audit linking. Remove the separate nonconformity and continual improvement pages, routes, and graph hooks. Update sidebar navigation, routes, and components for nullable audit framework field. Signed-off-by: Bryan Frimin --- .../src/components/form/AuditSelectField.tsx | 4 +- .../components/risks/LinkedRisksDialog.tsx | 1 + apps/console/src/hooks/graph/AuditGraph.ts | 4 +- .../hooks/graph/ContinualImprovementGraph.ts | 232 -------- .../src/hooks/graph/NonconformityGraph.ts | 261 --------- .../iam/organizations/_components/Sidebar.tsx | 23 +- .../CompliancePageAuditListItem.tsx | 2 +- .../ContinualImprovementDetailsPage.tsx | 339 ----------- .../ContinualImprovementsPage.tsx | 311 ---------- .../CreateContinualImprovementDialog.tsx | 246 -------- .../findings/FindingDetailsPage.tsx | 513 ++++++++++++++++ .../findings/FindingDetailsPageLoader.tsx | 30 + .../organizations/findings/FindingsPage.tsx | 549 ++++++++++++++++++ .../findings/FindingsPageLoader.tsx | 33 ++ .../findings/dialogs/CreateFindingDialog.tsx | 352 +++++++++++ .../nonconformities/NonconformitiesPage.tsx | 328 ----------- .../NonconformityDetailsPage.tsx | 309 ---------- .../dialogs/CreateNonconformityDialog.tsx | 253 -------- apps/console/src/routes.tsx | 6 +- .../src/routes/continualImprovementRoutes.ts | 97 ---- apps/console/src/routes/findingRoutes.ts | 39 ++ .../console/src/routes/nonconformityRoutes.ts | 98 ---- 22 files changed, 1530 insertions(+), 2500 deletions(-) delete mode 100644 apps/console/src/hooks/graph/ContinualImprovementGraph.ts delete mode 100644 apps/console/src/hooks/graph/NonconformityGraph.ts delete mode 100644 apps/console/src/pages/organizations/continualImprovements/ContinualImprovementDetailsPage.tsx delete mode 100644 apps/console/src/pages/organizations/continualImprovements/ContinualImprovementsPage.tsx delete mode 100644 apps/console/src/pages/organizations/continualImprovements/dialogs/CreateContinualImprovementDialog.tsx create mode 100644 apps/console/src/pages/organizations/findings/FindingDetailsPage.tsx create mode 100644 apps/console/src/pages/organizations/findings/FindingDetailsPageLoader.tsx create mode 100644 apps/console/src/pages/organizations/findings/FindingsPage.tsx create mode 100644 apps/console/src/pages/organizations/findings/FindingsPageLoader.tsx create mode 100644 apps/console/src/pages/organizations/findings/dialogs/CreateFindingDialog.tsx delete mode 100644 apps/console/src/pages/organizations/nonconformities/NonconformitiesPage.tsx delete mode 100644 apps/console/src/pages/organizations/nonconformities/NonconformityDetailsPage.tsx delete mode 100644 apps/console/src/pages/organizations/nonconformities/dialogs/CreateNonconformityDialog.tsx delete mode 100644 apps/console/src/routes/continualImprovementRoutes.ts create mode 100644 apps/console/src/routes/findingRoutes.ts delete mode 100644 apps/console/src/routes/nonconformityRoutes.ts diff --git a/apps/console/src/components/form/AuditSelectField.tsx b/apps/console/src/components/form/AuditSelectField.tsx index 0303fe788..3e93a1a2f 100644 --- a/apps/console/src/components/form/AuditSelectField.tsx +++ b/apps/console/src/components/form/AuditSelectField.tsx @@ -105,8 +105,8 @@ function AuditSelectWithQuery(
{audit.name - ? `${audit.framework.name} - ${audit.name}` - : audit.framework.name} + ? `${audit.framework?.name} - ${audit.name}` + : audit.framework?.name}
diff --git a/apps/console/src/components/risks/LinkedRisksDialog.tsx b/apps/console/src/components/risks/LinkedRisksDialog.tsx index 8582ae91d..94b7055f9 100644 --- a/apps/console/src/components/risks/LinkedRisksDialog.tsx +++ b/apps/console/src/components/risks/LinkedRisksDialog.tsx @@ -136,6 +136,7 @@ type RowProps = { name: string; category: string; id: string; + description?: string | null; }; linkedRisks: Set; disabled?: boolean; diff --git a/apps/console/src/hooks/graph/AuditGraph.ts b/apps/console/src/hooks/graph/AuditGraph.ts index 1936c91b3..5a554d234 100644 --- a/apps/console/src/hooks/graph/AuditGraph.ts +++ b/apps/console/src/hooks/graph/AuditGraph.ts @@ -121,7 +121,7 @@ export const deleteAuditMutation = graphql` `; export const useDeleteAudit = ( - audit: { id: string; framework: { name: string } }, + audit: { id: string; framework?: { name: string } | null }, connectionId: string, onSuccess?: () => void, ) => { @@ -150,7 +150,7 @@ export const useDeleteAudit = ( __( "This will permanently delete the audit for %s. This action cannot be undone.", ), - audit.framework.name, + audit.framework?.name ?? "", ), }, ); diff --git a/apps/console/src/hooks/graph/ContinualImprovementGraph.ts b/apps/console/src/hooks/graph/ContinualImprovementGraph.ts deleted file mode 100644 index cb502df9c..000000000 --- a/apps/console/src/hooks/graph/ContinualImprovementGraph.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { promisifyMutation, sprintf } from "@probo/helpers"; -import { useTranslate } from "@probo/i18n"; -import { useConfirm } from "@probo/ui"; -import { useMutation } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { useMutationWithToasts } from "../useMutationWithToasts"; - -/* eslint-disable relay/unused-fields, relay/must-colocate-fragment-spreads */ - -export const ContinualImprovementsConnectionKey - = "ContinualImprovementsPage_continualImprovements"; - -export const continualImprovementsQuery = graphql` - query ContinualImprovementGraphListQuery( - $organizationId: ID! - $snapshotId: ID - ) { - node(id: $organizationId) { - ... on Organization { - canCreateContinualImprovement: permission( - action: "core:continual-improvement:create" - ) - ...ContinualImprovementsPageFragment @arguments(snapshotId: $snapshotId) - } - } - } -`; - -export const continualImprovementNodeQuery = graphql` - query ContinualImprovementGraphNodeQuery($continualImprovementId: ID!) { - node(id: $continualImprovementId) { - ... on ContinualImprovement { - id - snapshotId - sourceId - referenceId - description - source - targetDate - status - priority - owner { - id - fullName - } - organization { - id - name - } - createdAt - updatedAt - canUpdate: permission(action: "core:continual-improvement:update") - canDelete: permission(action: "core:continual-improvement:delete") - } - } - } -`; - -export const createContinualImprovementMutation = graphql` - mutation ContinualImprovementGraphCreateMutation( - $input: CreateContinualImprovementInput! - $connections: [ID!]! - ) { - createContinualImprovement(input: $input) { - continualImprovementEdge @prependEdge(connections: $connections) { - node { - id - referenceId - description - source - targetDate - status - priority - owner { - id - fullName - } - createdAt - canUpdate: permission(action: "core:continual-improvement:update") - canDelete: permission(action: "core:continual-improvement:delete") - } - } - } - } -`; - -export const updateContinualImprovementMutation = graphql` - mutation ContinualImprovementGraphUpdateMutation( - $input: UpdateContinualImprovementInput! - ) { - updateContinualImprovement(input: $input) { - continualImprovement { - id - referenceId - description - source - targetDate - status - priority - owner { - id - fullName - } - updatedAt - } - } - } -`; - -export const deleteContinualImprovementMutation = graphql` - mutation ContinualImprovementGraphDeleteMutation( - $input: DeleteContinualImprovementInput! - $connections: [ID!]! - ) { - deleteContinualImprovement(input: $input) { - deletedContinualImprovementId @deleteEdge(connections: $connections) - } - } -`; - -export const useDeleteContinualImprovement = ( - improvement: { id: string; referenceId: string }, - connectionId: string, -) => { - const { __ } = useTranslate(); - const [mutate] = useMutationWithToasts(deleteContinualImprovementMutation, { - successMessage: __("Continual improvement deleted successfully"), - errorMessage: __("Failed to delete continual improvement"), - }); - const confirm = useConfirm(); - - return () => { - confirm( - () => - mutate({ - variables: { - input: { - continualImprovementId: improvement.id, - }, - connections: [connectionId], - }, - }), - { - message: sprintf( - __( - "This will permanently delete the continual improvement %s. This action cannot be undone.", - ), - improvement.referenceId, - ), - }, - ); - }; -}; - -export const useCreateContinualImprovement = (connectionId: string) => { - // eslint-disable-next-line relay/generated-typescript-types - const [mutate] = useMutation(createContinualImprovementMutation); - const { __ } = useTranslate(); - - return (input: { - organizationId: string; - referenceId: string; - description?: string; - source?: string; - ownerId: string; - targetDate?: string; - status: string; - priority: string; - }) => { - if (!input.organizationId) { - return alert( - __("Failed to create continual improvement: organization is required"), - ); - } - if (!input.referenceId) { - return alert( - __("Failed to create continual improvement: reference ID is required"), - ); - } - if (!input.ownerId) { - return alert( - __("Failed to create continual improvement: owner is required"), - ); - } - - return promisifyMutation(mutate)({ - variables: { - input: { - organizationId: input.organizationId, - referenceId: input.referenceId, - description: input.description, - source: input.source, - ownerId: input.ownerId, - targetDate: input.targetDate, - status: input.status || "OPEN", - priority: input.priority || "MEDIUM", - }, - connections: [connectionId], - }, - }); - }; -}; - -export const useUpdateContinualImprovement = () => { - // eslint-disable-next-line relay/generated-typescript-types - const [mutate] = useMutation(updateContinualImprovementMutation); - const { __ } = useTranslate(); - - return (input: { - id: string; - referenceId?: string; - description?: string; - source?: string; - ownerId?: string; - targetDate?: string | null; - status?: string; - priority?: string; - }) => { - if (!input.id) { - return alert( - __("Failed to update continual improvement: ID is required"), - ); - } - - return promisifyMutation(mutate)({ - variables: { - input, - }, - }); - }; -}; diff --git a/apps/console/src/hooks/graph/NonconformityGraph.ts b/apps/console/src/hooks/graph/NonconformityGraph.ts deleted file mode 100644 index c40c39de6..000000000 --- a/apps/console/src/hooks/graph/NonconformityGraph.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { promisifyMutation, sprintf } from "@probo/helpers"; -import { useTranslate } from "@probo/i18n"; -import { useConfirm } from "@probo/ui"; -import { useMutation } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { useMutationWithToasts } from "../useMutationWithToasts"; - -/* eslint-disable relay/unused-fields, relay/must-colocate-fragment-spreads */ - -export const NonconformitiesConnectionKey - = "NonconformitiesPage_nonconformities"; - -export const nonconformitiesQuery = graphql` - query NonconformityGraphListQuery($organizationId: ID!, $snapshotId: ID) { - node(id: $organizationId) { - ... on Organization { - canCreateNonconformity: permission(action: "core:nonconformity:create") - ...NonconformitiesPageFragment @arguments(snapshotId: $snapshotId) - } - } - } -`; - -export const nonconformityNodeQuery = graphql` - query NonconformityGraphNodeQuery($nonconformityId: ID!) { - node(id: $nonconformityId) { - ... on Nonconformity { - id - snapshotId - referenceId - description - dateIdentified - rootCause - correctiveAction - dueDate - status - effectivenessCheck - audit { - id - framework { - id - name - } - } - owner { - id - fullName - } - organization { - id - name - } - createdAt - updatedAt - canUpdate: permission(action: "core:nonconformity:update") - canDelete: permission(action: "core:nonconformity:delete") - } - } - } -`; - -export const createNonconformityMutation = graphql` - mutation NonconformityGraphCreateMutation( - $input: CreateNonconformityInput! - $connections: [ID!]! - ) { - createNonconformity(input: $input) { - nonconformityEdge @prependEdge(connections: $connections) { - node { - id - referenceId - description - status - dateIdentified - dueDate - rootCause - audit { - id - framework { - name - } - } - owner { - id - fullName - } - createdAt - canUpdate: permission(action: "core:nonconformity:update") - canDelete: permission(action: "core:nonconformity:delete") - } - } - } - } -`; - -export const updateNonconformityMutation = graphql` - mutation NonconformityGraphUpdateMutation($input: UpdateNonconformityInput!) { - updateNonconformity(input: $input) { - nonconformity { - id - referenceId - description - dateIdentified - rootCause - correctiveAction - dueDate - status - effectivenessCheck - owner { - id - fullName - } - audit { - id - framework { - id - name - } - } - updatedAt - } - } - } -`; - -export const deleteNonconformityMutation = graphql` - mutation NonconformityGraphDeleteMutation( - $input: DeleteNonconformityInput! - $connections: [ID!]! - ) { - deleteNonconformity(input: $input) { - deletedNonconformityId @deleteEdge(connections: $connections) - } - } -`; - -export const useDeleteNonconformity = ( - nonconformity: { id: string; referenceId: string }, - connectionId: string, -) => { - const { __ } = useTranslate(); - const [mutate] = useMutationWithToasts(deleteNonconformityMutation, { - successMessage: __("Nonconformity deleted successfully"), - errorMessage: __("Failed to delete nonconformity"), - }); - const confirm = useConfirm(); - - return () => { - confirm( - () => - mutate({ - variables: { - input: { - nonconformityId: nonconformity.id, - }, - connections: [connectionId], - }, - }), - { - message: sprintf( - __( - "This will permanently delete the nonconformity %s. This action cannot be undone.", - ), - nonconformity.referenceId, - ), - }, - ); - }; -}; - -export const useCreateNonconformity = (connectionId: string) => { - // eslint-disable-next-line relay/generated-typescript-types - const [mutate] = useMutation(createNonconformityMutation); - const { __ } = useTranslate(); - - return (input: { - organizationId: string; - referenceId: string; - description?: string; - auditId?: string; - dateIdentified?: string; - rootCause: string; - correctiveAction?: string; - ownerId: string; - dueDate?: string; - status: string; - effectivenessCheck?: string; - }) => { - if (!input.organizationId) { - return alert( - __("Failed to create nonconformity: organization is required"), - ); - } - if (!input.referenceId) { - return alert( - __("Failed to create nonconformity: reference ID is required"), - ); - } - if (!input.ownerId) { - return alert(__("Failed to create nonconformity: owner is required")); - } - if (!input.rootCause) { - return alert( - __("Failed to create nonconformity: root cause is required"), - ); - } - - return promisifyMutation(mutate)({ - variables: { - input: { - organizationId: input.organizationId, - referenceId: input.referenceId, - description: input.description, - auditId: input.auditId || undefined, - dateIdentified: input.dateIdentified, - rootCause: input.rootCause, - correctiveAction: input.correctiveAction, - ownerId: input.ownerId, - dueDate: input.dueDate, - status: input.status || "OPEN", - effectivenessCheck: input.effectivenessCheck, - }, - connections: [connectionId], - }, - }); - }; -}; - -export const useUpdateNonconformity = () => { - // eslint-disable-next-line relay/generated-typescript-types - const [mutate] = useMutation(updateNonconformityMutation); - const { __ } = useTranslate(); - - return (input: { - id: string; - referenceId?: string; - description?: string; - dateIdentified?: string | null; - rootCause?: string; - correctiveAction?: string; - ownerId?: string; - auditId?: string | null; - dueDate?: string | null; - status?: string; - effectivenessCheck?: string; - }) => { - if (!input.id) { - return alert(__("Failed to update nonconformity: ID is required")); - } - - return promisifyMutation(mutate)({ - variables: { - input: { - ...input, - auditId: input.auditId || null, - }, - }, - }); - }; -}; diff --git a/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx b/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx index a5f280c17..6406483e1 100644 --- a/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx +++ b/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx @@ -6,16 +6,15 @@ import { IconCalendar1, IconCircleProgress, IconClock, - IconCrossLargeX, IconFire3, IconGroup1, IconInboxEmpty, IconListStack, IconLock, + IconMagnifyingGlass, IconMedal, IconPageCheck, IconPageTextLine, - IconRotateCw, IconSettingsGear2, IconShield, IconStore, @@ -41,11 +40,8 @@ const fragment = graphql` canListAssets: permission(action: "core:asset:list") canListData: permission(action: "core:datum:list") canListAudits: permission(action: "core:audit:list") - canListNonconformities: permission(action: "core:nonconformity:list") + canListFindings: permission(action: "core:finding:list") canListObligations: permission(action: "core:obligation:list") - canListContinualImprovements: permission( - action: "core:continual-improvement:list" - ) canListProcessingActivities: permission( action: "core:processing-activity:list" ) @@ -148,11 +144,11 @@ export function Sidebar(props: { fKey: SidebarFragment$key }) { to={`${prefix}/audits`} /> )} - {organization.canListNonconformities && ( + {organization.canListFindings && ( )} {organization.canListObligations && ( @@ -162,13 +158,6 @@ export function Sidebar(props: { fKey: SidebarFragment$key }) { to={`${prefix}/obligations`} /> )} - {organization.canListContinualImprovements && ( - - )} {organization.canListProcessingActivities && ( -
{audit.framework.name}
+
{audit.framework?.name}
{audit.name || __("Untitled")} {validUntilFormatted} diff --git a/apps/console/src/pages/organizations/continualImprovements/ContinualImprovementDetailsPage.tsx b/apps/console/src/pages/organizations/continualImprovements/ContinualImprovementDetailsPage.tsx deleted file mode 100644 index 95dcb600e..000000000 --- a/apps/console/src/pages/organizations/continualImprovements/ContinualImprovementDetailsPage.tsx +++ /dev/null @@ -1,339 +0,0 @@ -import { formatError, type GraphQLError } from "@probo/helpers"; -import { - formatDatetime, - getStatusLabel, - getStatusVariant, - validateSnapshotConsistency, -} from "@probo/helpers"; -import { useTranslate } from "@probo/i18n"; -import { - ActionDropdown, - Badge, - Breadcrumb, - Button, - Card, - DropdownItem, - Field, - Input, - Label, - Option, - Select, - Textarea, - useToast, -} from "@probo/ui"; -import { Controller } from "react-hook-form"; -import { - ConnectionHandler, - type PreloadedQuery, - usePreloadedQuery, -} from "react-relay"; -import { useParams } from "react-router"; -import { z } from "zod"; - -import type { ContinualImprovementGraphNodeQuery } from "#/__generated__/core/ContinualImprovementGraphNodeQuery.graphql"; -import { PeopleSelectField } from "#/components/form/PeopleSelectField"; -import { SnapshotBanner } from "#/components/SnapshotBanner"; -import { useFormWithSchema } from "#/hooks/useFormWithSchema"; -import { useOrganizationId } from "#/hooks/useOrganizationId"; - -import { - continualImprovementNodeQuery, - ContinualImprovementsConnectionKey, - useDeleteContinualImprovement, - useUpdateContinualImprovement, -} from "../../../hooks/graph/ContinualImprovementGraph"; - -const updateImprovementSchema = z.object({ - referenceId: z.string().min(1, "Reference ID is required"), - description: z.string().optional(), - source: z.string().optional(), - targetDate: z.string().optional(), - status: z.enum(["OPEN", "IN_PROGRESS", "CLOSED"]), - priority: z.enum(["LOW", "MEDIUM", "HIGH"]), - ownerId: z.string().min(1, "Owner is required"), -}); - -type Props = { - queryRef: PreloadedQuery; -}; - -export default function ContinualImprovementDetailsPage(props: Props) { - const { node: improvement } - = usePreloadedQuery( - continualImprovementNodeQuery, - props.queryRef, - ); - const { __ } = useTranslate(); - const { toast } = useToast(); - const organizationId = useOrganizationId(); - const { snapshotId } = useParams<{ snapshotId?: string }>(); - const isSnapshotMode = Boolean(snapshotId); - - validateSnapshotConsistency(improvement, snapshotId); - - const updateImprovement = useUpdateContinualImprovement(); - - const connectionId = ConnectionHandler.getConnectionID( - organizationId, - ContinualImprovementsConnectionKey, - { filter: { snapshotId: snapshotId || null } }, - ); - - const deleteImprovement = useDeleteContinualImprovement( - { id: improvement.id!, referenceId: improvement.referenceId! }, - connectionId, - ); - - const { register, handleSubmit, formState, control } = useFormWithSchema( - updateImprovementSchema, - { - defaultValues: { - referenceId: improvement.referenceId || "", - description: improvement.description || "", - source: improvement.source || "", - targetDate: improvement.targetDate - ? new Date(improvement.targetDate).toISOString().split("T")[0] - : "", - status: improvement.status || "OPEN", - priority: improvement.priority || "MEDIUM", - ownerId: improvement.owner?.id || "", - }, - }, - ); - - const onSubmit = handleSubmit(async (formData) => { - try { - await updateImprovement({ - id: improvement.id!, - referenceId: formData.referenceId, - description: formData.description || undefined, - source: formData.source || undefined, - targetDate: formatDatetime(formData.targetDate) ?? null, - status: formData.status, - priority: formData.priority, - ownerId: formData.ownerId, - }); - - toast({ - title: __("Success"), - description: __("Continual improvement entry updated successfully"), - variant: "success", - }); - } catch (error) { - toast({ - title: __("Error"), - description: formatError( - __("Failed to update continual improvement"), - error as GraphQLError, - ), - variant: "error", - }); - } - }); - - const statusOptions = [ - { value: "OPEN", label: __("Open") }, - { value: "IN_PROGRESS", label: __("In Progress") }, - { value: "CLOSED", label: __("Closed") }, - ]; - - const priorityOptions = [ - { value: "LOW", label: __("Low") }, - { value: "MEDIUM", label: __("Medium") }, - { value: "HIGH", label: __("High") }, - ]; - - const breadcrumbImprovementsUrl = isSnapshotMode - ? `/organizations/${organizationId}/snapshots/${snapshotId}/continual-improvements` - : `/organizations/${organizationId}/continual-improvements`; - - return ( -
- {isSnapshotMode && snapshotId && ( - - )} -
- - {!isSnapshotMode && improvement.canDelete && ( - - - {__("Delete")} - - - )} -
- - -
-
-
-

{improvement.referenceId}

- - {getStatusLabel(improvement.status || "OPEN")} - - - {improvement.priority === "HIGH" - ? __("High") - : improvement.priority === "MEDIUM" - ? __("Medium") - : __("Low")} - -
-
- -
void onSubmit(e)} className="space-y-4"> - - -
- -