Add implemented state and justification to controls

Introduce `implemented` enum (IMPLEMENTED/NOT_IMPLEMENTED) and
`not_implemented_justification` (nullable text) fields on the Control
entity across all API surfaces (GraphQL, MCP, CLI), database, frontend,
and SOA export.

The database stores implementation state as a PostgreSQL enum
`control_implementation_state`. Controls default to IMPLEMENTED during
migration. The SOA list and PDF export show implementation status
alongside applicability, with "-" for non-applicable controls.
Justification columns are renamed for clarity: "Justification for
non-applicability" and "Justification for non-implementation".

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-03-16 19:24:34 +01:00
parent d8670d2412
commit cf1dadc0b5
25 changed files with 663 additions and 233 deletions

View File

@@ -103,6 +103,9 @@ export const frameworkControlNodeQuery = graphql`
name
sectionTitle
description
bestPractice
implemented
notImplementedJustification
canUpdate: permission(action: "core:control:update")
canDelete: permission(action: "core:control:delete")
canCreateMeasureMapping: permission(

View File

@@ -3,7 +3,9 @@ import { promisifyMutation } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import {
ActionDropdown,
Badge,
Button,
Card,
DropdownItem,
IconPencil,
IconTrashCan,
@@ -354,6 +356,28 @@ export default function FrameworkControlPage({ queryRef }: Props) {
{control.description}
</div>
)}
<Card padded className="mb-6 mt-6">
<div className="space-y-3">
<div className="flex items-center gap-2">
<span className="text-sm text-txt-secondary">{__("Best Practice")}</span>
<Badge variant={control.bestPractice ? "success" : "neutral"} size="sm">
{control.bestPractice ? __("Yes") : __("No")}
</Badge>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-txt-secondary">{__("Implemented")}</span>
<Badge variant={control.implemented === "IMPLEMENTED" ? "success" : "warning"} size="sm">
{control.implemented === "IMPLEMENTED" ? __("Implemented") : __("Not Implemented")}
</Badge>
</div>
{control.implemented === "NOT_IMPLEMENTED" && control.notImplementedJustification && (
<div>
<span className="text-xs text-txt-secondary">{__("Justification for non-implementation")}</span>
<div className="text-sm mt-0.5 whitespace-pre-wrap">{control.notImplementedJustification}</div>
</div>
)}
</div>
</Card>
<div className="mb-4">
<LinkedMeasuresCard
variant="card"

View File

@@ -34,6 +34,8 @@ const controlFragment = graphql`
description
sectionTitle
bestPractice
implemented
notImplementedJustification
}
`;
@@ -67,6 +69,8 @@ const schema = z.object({
description: z.string().optional().nullable(),
sectionTitle: z.string(),
bestPractice: z.boolean(),
implemented: z.enum(["IMPLEMENTED", "NOT_IMPLEMENTED"]),
notImplementedJustification: z.string().optional().nullable(),
});
export function FrameworkControlDialog(props: Props) {
@@ -91,6 +95,8 @@ export function FrameworkControlDialog(props: Props) {
description: frameworkControl?.description ?? "",
sectionTitle: frameworkControl?.sectionTitle ?? "",
bestPractice: frameworkControl?.bestPractice ?? true,
implemented: frameworkControl?.implemented ?? "IMPLEMENTED",
notImplementedJustification: frameworkControl?.notImplementedJustification ?? "",
}),
[frameworkControl],
);
@@ -105,10 +111,10 @@ export function FrameworkControlDialog(props: Props) {
}, [defaultValues, reset]);
const bestPracticeValue = watch("bestPractice");
const implementedValue = watch("implemented");
const onSubmit = async (data: z.infer<typeof schema>) => {
if (frameworkControl) {
// Update the control
await mutate({
variables: {
input: {
@@ -117,11 +123,12 @@ export function FrameworkControlDialog(props: Props) {
description: data.description || null,
sectionTitle: data.sectionTitle,
bestPractice: data.bestPractice,
implemented: data.implemented,
notImplementedJustification: data.implemented === "IMPLEMENTED" ? null : (data.notImplementedJustification || null),
},
},
});
} else {
// Create a new control
await mutate({
variables: {
input: {
@@ -130,6 +137,8 @@ export function FrameworkControlDialog(props: Props) {
description: data.description || null,
sectionTitle: data.sectionTitle,
bestPractice: data.bestPractice ?? true,
implemented: data.implemented ?? "IMPLEMENTED",
notImplementedJustification: data.implemented === "IMPLEMENTED" ? null : (data.notImplementedJustification || null),
},
connections: [props.connectionId!],
},
@@ -167,7 +176,7 @@ export function FrameworkControlDialog(props: Props) {
id="title"
required
variant="title"
placeholder={__("Document title")}
placeholder={__("Control name")}
{...register("name")}
/>
<Textarea
@@ -177,14 +186,33 @@ export function FrameworkControlDialog(props: Props) {
placeholder={__("Add description")}
{...register("description")}
/>
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={bestPracticeValue}
onChange={checked =>
setValue("bestPractice", checked)}
/>
<span className="text-sm">{__("Best Practice")}</span>
</label>
<div className="border border-border-low rounded-xl p-3 space-y-3 mt-4">
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={bestPracticeValue}
onChange={checked =>
setValue("bestPractice", checked)}
/>
<span className="text-sm">{__("Best Practice")}</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={implementedValue === "IMPLEMENTED"}
onChange={checked =>
setValue("implemented", checked ? "IMPLEMENTED" : "NOT_IMPLEMENTED")}
/>
<span className="text-sm">{__("Implemented")}</span>
</label>
{implementedValue === "NOT_IMPLEMENTED" && (
<Textarea
id="notImplementedJustification"
variant="ghost"
autogrow
placeholder={__("Justification for non-implementation")}
{...register("notImplementedJustification")}
/>
)}
</div>
</DialogContent>
<DialogFooter>
<Button type="submit" disabled={isMutating}>

View File

@@ -79,6 +79,8 @@ const createApplicabilityStatementMutation = graphql`
sectionTitle
name
bestPractice
implemented
notImplementedJustification
regulatory
contractual
riskAssessment

View File

@@ -71,6 +71,8 @@ export const controlsFragment = graphql`
sectionTitle
name
bestPractice
implemented
notImplementedJustification
regulatory
contractual
riskAssessment
@@ -135,6 +137,8 @@ export default function StateOfApplicabilityControlsTab({
applicability: edge.node.applicability,
justification: edge.node.justification,
bestPractice: edge.node.control.bestPractice,
implemented: edge.node.control.implemented,
notImplementedJustification: edge.node.control.notImplementedJustification,
regulatory: edge.node.control.regulatory,
contractual: edge.node.control.contractual,
riskAssessment: edge.node.control.riskAssessment,
@@ -204,29 +208,19 @@ export default function StateOfApplicabilityControlsTab({
</div>
)}
<Table>
<Table className="table-fixed w-full">
<Thead>
<Tr>
<Th className="w-32">{__("Framework")}</Th>
<Th>{__("Control")}</Th>
<Th className="w-28 text-center">
{__("Applicability")}
</Th>
<Th className="min-w-48">{__("Justification")}</Th>
<Th className="w-24 text-center">
{__("Regulatory")}
</Th>
<Th className="w-24 text-center">
{__("Contractual")}
</Th>
<Th className="w-32 text-center">
{__("Best Practice")}
</Th>
<Th className="w-36 text-center">
{__("Risk Assessment")}
</Th>
<Th className="w-[10%]">{__("Framework")}</Th>
<Th className="w-[20%]">{__("Control")}</Th>
<Th className="w-[15%]">{__("Applicability")}</Th>
<Th className="w-[15%]">{__("Implemented")}</Th>
<Th className="w-[8%]">{__("Regulatory")}</Th>
<Th className="w-[8%]">{__("Contractual")}</Th>
<Th className="w-[8%]">{__("Best Practice")}</Th>
<Th className="w-[8%]">{__("Risk Assessment")}</Th>
{(canUpdate || canDelete) && (
<Th className="w-12"></Th>
<Th className="w-[4%]"></Th>
)}
</Tr>
</Thead>
@@ -250,103 +244,82 @@ export default function StateOfApplicabilityControlsTab({
{control.frameworkName}
</Td>
<Td>
<div className="space-y-1">
<div className="text-xs font-medium text-txt-tertiary">
<div className="space-y-0.5">
<div className="text-xs text-txt-tertiary">
{control.sectionTitle}
</div>
<div className="text-sm">
<div className="text-xs">
{control.name}
</div>
</div>
</Td>
<Td>
<div className="flex justify-center">
<div className="space-y-1">
{control.applicability !== null
? (
<Badge
variant={
control.applicability
? "success"
: "danger"
}
variant={control.applicability ? "success" : "danger"}
size="sm"
>
{control.applicability
? __("Yes")
: __("No")}
{control.applicability ? __("Yes") : __("No")}
</Badge>
)
: (
<span className="text-txt-tertiary">
-
</span>
<span className="text-txt-tertiary">-</span>
)}
</div>
</Td>
<Td>
<div className="text-sm text-txt-secondary line-clamp-2">
{control.justification || (
<span className="text-txt-tertiary italic">
-
</span>
{control.justification && (
<p className="text-xs text-txt-secondary break-words">
{control.justification}
</p>
)}
</div>
</Td>
<Td>
<div className="flex justify-center">
{control.applicability === false
? <span className="text-txt-tertiary">-</span>
: (
{control.applicability === false
? <span className="text-txt-tertiary">-</span>
: (
<div className="space-y-1">
<Badge
variant={control.regulatory ? "success" : "danger"}
variant={control.implemented === "IMPLEMENTED" ? "success" : "danger"}
size="sm"
>
{control.regulatory ? __("Yes") : __("No")}
{control.implemented === "IMPLEMENTED" ? __("Yes") : __("No")}
</Badge>
)}
</div>
{control.implemented === "NOT_IMPLEMENTED" && control.notImplementedJustification && (
<p className="text-xs text-txt-secondary break-words">
{control.notImplementedJustification}
</p>
)}
</div>
)}
</Td>
<Td>
<div className="flex justify-center">
{control.applicability === false
? <span className="text-txt-tertiary">-</span>
: (
<Badge
variant={control.contractual ? "success" : "danger"}
size="sm"
>
{control.contractual ? __("Yes") : __("No")}
</Badge>
)}
</div>
{control.applicability === false
? <span className="text-txt-tertiary">-</span>
: control.regulatory
? <Badge variant="success" size="sm">{__("Yes")}</Badge>
: <Badge variant="danger" size="sm">{__("No")}</Badge>}
</Td>
<Td>
<div className="flex justify-center">
{control.applicability === false
? <span className="text-txt-tertiary">-</span>
: (
<Badge
variant={control.bestPractice ? "success" : "danger"}
size="sm"
>
{control.bestPractice ? __("Yes") : __("No")}
</Badge>
)}
</div>
{control.applicability === false
? <span className="text-txt-tertiary">-</span>
: control.contractual
? <Badge variant="success" size="sm">{__("Yes")}</Badge>
: <Badge variant="danger" size="sm">{__("No")}</Badge>}
</Td>
<Td>
<div className="flex justify-center">
{control.applicability === false
? <span className="text-txt-tertiary">-</span>
: (
<Badge
variant={control.riskAssessment ? "success" : "danger"}
size="sm"
>
{control.riskAssessment ? __("Yes") : __("No")}
</Badge>
)}
</div>
{control.applicability === false
? <span className="text-txt-tertiary">-</span>
: control.bestPractice
? <Badge variant="success" size="sm">{__("Yes")}</Badge>
: <Badge variant="danger" size="sm">{__("No")}</Badge>}
</Td>
<Td>
{control.applicability === false
? <span className="text-txt-tertiary">-</span>
: control.riskAssessment
? <Badge variant="success" size="sm">{__("Yes")}</Badge>
: <Badge variant="danger" size="sm">{__("No")}</Badge>}
</Td>
{(canUpdate || canDelete) && (
<Td noLink className="text-end">