diff --git a/apps/console/src/hooks/graph/VendorGraph.ts b/apps/console/src/hooks/graph/VendorGraph.ts index 3d0326587..8d037fe90 100644 --- a/apps/console/src/hooks/graph/VendorGraph.ts +++ b/apps/console/src/hooks/graph/VendorGraph.ts @@ -153,6 +153,7 @@ export const vendorNodeQuery = graphql` ...useVendorFormFragment ...VendorComplianceTabFragment ...VendorRiskAssessmentTabFragment + ...VendorOverviewTabBusinessAssociateAgreementFragment } } viewer { diff --git a/apps/console/src/hooks/graph/__generated__/VendorGraphNodeQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/VendorGraphNodeQuery.graphql.ts index 43e6b5da3..68236ba76 100644 --- a/apps/console/src/hooks/graph/__generated__/VendorGraphNodeQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/VendorGraphNodeQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<2b4b64bb6df013e073b34e8249232ab2>> + * @generated SignedSource<<9174011c1abd05e499786607130d0c82>> * @lightSyntaxTransform * @nogrep */ @@ -19,7 +19,7 @@ export type VendorGraphNodeQuery$data = { readonly id?: string; readonly name?: string; readonly websiteUrl?: string | null | undefined; - readonly " $fragmentSpreads": FragmentRefs<"VendorComplianceTabFragment" | "VendorRiskAssessmentTabFragment" | "useVendorFormFragment">; + readonly " $fragmentSpreads": FragmentRefs<"VendorComplianceTabFragment" | "VendorOverviewTabBusinessAssociateAgreementFragment" | "VendorRiskAssessmentTabFragment" | "useVendorFormFragment">; }; readonly viewer: { readonly user: { @@ -110,38 +110,52 @@ v10 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "cursor", + "name": "validUntil", "storageKey": null }, v11 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "endCursor", + "name": "fileUrl", "storageKey": null }, v12 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "hasNextPage", + "name": "cursor", "storageKey": null }, v13 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "hasPreviousPage", + "name": "endCursor", "storageKey": null }, v14 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "startCursor", + "name": "hasNextPage", "storageKey": null }, v15 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null +}, +v16 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null +}, +v17 = { "kind": "ClientExtension", "selections": [ { @@ -153,7 +167,7 @@ v15 = { } ] }, -v16 = [ +v18 = [ "orderBy" ]; return { @@ -194,6 +208,11 @@ return { "args": null, "kind": "FragmentSpread", "name": "VendorRiskAssessmentTabFragment" + }, + { + "args": null, + "kind": "FragmentSpread", + "name": "VendorOverviewTabBusinessAssociateAgreementFragment" } ], "type": "Vendor", @@ -382,13 +401,7 @@ return { "name": "reportDate", "storageKey": null }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "validUntil", - "storageKey": null - }, + (v10/*: any*/), { "alias": null, "args": null, @@ -396,13 +409,7 @@ return { "name": "reportName", "storageKey": null }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "fileUrl", - "storageKey": null - }, + (v11/*: any*/), { "alias": null, "args": null, @@ -414,7 +421,7 @@ return { ], "storageKey": null }, - (v10/*: any*/) + (v12/*: any*/) ], "storageKey": null }, @@ -426,21 +433,21 @@ return { "name": "pageInfo", "plural": false, "selections": [ - (v11/*: any*/), - (v12/*: any*/), (v13/*: any*/), - (v14/*: any*/) + (v14/*: any*/), + (v15/*: any*/), + (v16/*: any*/) ], "storageKey": null }, - (v15/*: any*/) + (v17/*: any*/) ], "storageKey": "complianceReports(first:50)" }, { "alias": null, "args": (v9/*: any*/), - "filters": (v16/*: any*/), + "filters": (v18/*: any*/), "handle": "connection", "key": "VendorComplianceTabFragment_complianceReports", "kind": "LinkedHandle", @@ -529,7 +536,7 @@ return { ], "storageKey": null }, - (v10/*: any*/) + (v12/*: any*/) ], "storageKey": null }, @@ -541,25 +548,60 @@ return { "name": "pageInfo", "plural": false, "selections": [ - (v12/*: any*/), - (v11/*: any*/), + (v14/*: any*/), (v13/*: any*/), - (v14/*: any*/) + (v15/*: any*/), + (v16/*: any*/) ], "storageKey": null }, - (v15/*: any*/) + (v17/*: any*/) ], "storageKey": "riskAssessments(first:50)" }, { "alias": null, "args": (v9/*: any*/), - "filters": (v16/*: any*/), + "filters": (v18/*: any*/), "handle": "connection", "key": "VendorRiskAssessmentTabFragment_riskAssessments", "kind": "LinkedHandle", "name": "riskAssessments" + }, + { + "alias": null, + "args": null, + "concreteType": "VendorBusinessAssociateAgreement", + "kind": "LinkedField", + "name": "businessAssociateAgreement", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fileName", + "storageKey": null + }, + (v11/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "validFrom", + "storageKey": null + }, + (v10/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + } + ], + "storageKey": null } ], "type": "Vendor", @@ -596,16 +638,16 @@ return { ] }, "params": { - "cacheID": "a57eb59eb17a2ad6d4f72f9174665561", + "cacheID": "95f9f21af1253d08c97dfd7d2f65f2e3", "id": null, "metadata": {}, "name": "VendorGraphNodeQuery", "operationKind": "query", - "text": "query VendorGraphNodeQuery(\n $vendorId: ID!\n $organizationId: ID!\n) {\n node(id: $vendorId) {\n __typename\n ... on Vendor {\n id\n name\n websiteUrl\n ...useVendorFormFragment\n ...VendorComplianceTabFragment\n ...VendorRiskAssessmentTabFragment\n }\n id\n }\n viewer {\n user {\n people(organizationId: $organizationId) {\n id\n }\n id\n }\n id\n }\n}\n\nfragment VendorComplianceTabFragment on Vendor {\n complianceReports(first: 50) {\n edges {\n node {\n id\n ...VendorComplianceTabFragment_report\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorComplianceTabFragment_report on VendorComplianceReport {\n id\n reportDate\n validUntil\n reportName\n fileUrl\n fileSize\n}\n\nfragment VendorRiskAssessmentTabFragment on Vendor {\n id\n riskAssessments(first: 50) {\n edges {\n node {\n id\n ...VendorRiskAssessmentTabFragment_assessment\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment VendorRiskAssessmentTabFragment_assessment on VendorRiskAssessment {\n id\n assessedAt\n assessedBy {\n id\n fullName\n }\n expiresAt\n dataSensitivity\n businessImpact\n notes\n}\n\nfragment useVendorFormFragment on Vendor {\n id\n name\n description\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n websiteUrl\n legalName\n headquarterAddress\n certifications\n securityPageUrl\n trustPageUrl\n businessOwner {\n id\n }\n securityOwner {\n id\n }\n}\n" + "text": "query VendorGraphNodeQuery(\n $vendorId: ID!\n $organizationId: ID!\n) {\n node(id: $vendorId) {\n __typename\n ... on Vendor {\n id\n name\n websiteUrl\n ...useVendorFormFragment\n ...VendorComplianceTabFragment\n ...VendorRiskAssessmentTabFragment\n ...VendorOverviewTabBusinessAssociateAgreementFragment\n }\n id\n }\n viewer {\n user {\n people(organizationId: $organizationId) {\n id\n }\n id\n }\n id\n }\n}\n\nfragment VendorComplianceTabFragment on Vendor {\n complianceReports(first: 50) {\n edges {\n node {\n id\n ...VendorComplianceTabFragment_report\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorComplianceTabFragment_report on VendorComplianceReport {\n id\n reportDate\n validUntil\n reportName\n fileUrl\n fileSize\n}\n\nfragment VendorOverviewTabBusinessAssociateAgreementFragment on Vendor {\n businessAssociateAgreement {\n id\n fileName\n fileUrl\n validFrom\n validUntil\n createdAt\n }\n}\n\nfragment VendorRiskAssessmentTabFragment on Vendor {\n id\n riskAssessments(first: 50) {\n edges {\n node {\n id\n ...VendorRiskAssessmentTabFragment_assessment\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment VendorRiskAssessmentTabFragment_assessment on VendorRiskAssessment {\n id\n assessedAt\n assessedBy {\n id\n fullName\n }\n expiresAt\n dataSensitivity\n businessImpact\n notes\n}\n\nfragment useVendorFormFragment on Vendor {\n id\n name\n description\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n websiteUrl\n legalName\n headquarterAddress\n certifications\n securityPageUrl\n trustPageUrl\n businessOwner {\n id\n }\n securityOwner {\n id\n }\n}\n" } }; })(); -(node as any).hash = "a4d080fd9c239011fa78c812153ed8da"; +(node as any).hash = "c80a167ea096212995f1d3261bc2bf57"; export default node; diff --git a/apps/console/src/pages/organizations/vendors/dialogs/DeleteBusinessAssociateAgreementDialog.tsx b/apps/console/src/pages/organizations/vendors/dialogs/DeleteBusinessAssociateAgreementDialog.tsx new file mode 100644 index 000000000..6c5910bae --- /dev/null +++ b/apps/console/src/pages/organizations/vendors/dialogs/DeleteBusinessAssociateAgreementDialog.tsx @@ -0,0 +1,89 @@ +import { + Button, + Dialog, + DialogContent, + DialogFooter, + Spinner, + useDialogRef, +} from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import { sprintf } from "@probo/helpers"; +import { graphql } from "react-relay"; +import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; + +const deleteBusinessAssociateAgreementMutation = graphql` + mutation DeleteBusinessAssociateAgreementDialogMutation( + $input: DeleteVendorBusinessAssociateAgreementInput! + ) { + deleteVendorBusinessAssociateAgreement(input: $input) { + deletedVendorId + } + } +`; + +type Props = { + children: React.ReactNode; + vendorId: string; + fileName: string; + onSuccess?: () => void; +}; + +export function DeleteBusinessAssociateAgreementDialog({ + children, + vendorId, + fileName, + onSuccess, +}: Props) { + const { __ } = useTranslate(); + const ref = useDialogRef(); + + const [mutate, isDeleting] = useMutationWithToasts(deleteBusinessAssociateAgreementMutation, { + successMessage: __("Business Associate Agreement deleted successfully"), + errorMessage: __("Failed to delete Business Associate Agreement"), + }); + + const handleDelete = async () => { + await mutate({ + variables: { + input: { + vendorId, + }, + }, + }); + + onSuccess?.(); + ref.current?.close(); + }; + + return ( + + +

+ {sprintf( + __("Are you sure you want to delete the Business Associate Agreement \"%s\"?"), + fileName + )} +

+

+ {__("This action cannot be undone.")} +

+
+ + + + +
+ ); +} diff --git a/apps/console/src/pages/organizations/vendors/dialogs/EditBusinessAssociateAgreementDialog.tsx b/apps/console/src/pages/organizations/vendors/dialogs/EditBusinessAssociateAgreementDialog.tsx new file mode 100644 index 000000000..238c2f7ab --- /dev/null +++ b/apps/console/src/pages/organizations/vendors/dialogs/EditBusinessAssociateAgreementDialog.tsx @@ -0,0 +1,135 @@ +import { + Button, + Dialog, + DialogContent, + DialogFooter, + Field, + Input, + Spinner, + useDialogRef, +} from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import { graphql } from "react-relay"; +import { z } from "zod"; +import { useFormWithSchema } from "/hooks/useFormWithSchema"; +import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; + +const updateBusinessAssociateAgreementMutation = graphql` + mutation EditBusinessAssociateAgreementDialogMutation( + $input: UpdateVendorBusinessAssociateAgreementInput! + ) { + updateVendorBusinessAssociateAgreement(input: $input) { + vendorBusinessAssociateAgreement { + id + fileUrl + validFrom + validUntil + createdAt + } + } + } +`; + +const schema = z.object({ + validFrom: z.string().optional(), + validUntil: z.string().optional(), +}); + +type Props = { + children: React.ReactNode; + vendorId: string; + agreement: { + validFrom?: string | null; + validUntil?: string | null; + }; + onSuccess?: () => void; +}; + +export function EditBusinessAssociateAgreementDialog({ + children, + vendorId, + agreement, + onSuccess, +}: Props) { + const { __ } = useTranslate(); + const ref = useDialogRef(); + + const formatDateForForm = (datetime?: string | null) => { + if (!datetime) return ""; + return datetime.split("T")[0]; + }; + + const { + register, + handleSubmit, + formState: { isSubmitting }, + reset, + } = useFormWithSchema(schema, { + defaultValues: { + validFrom: formatDateForForm(agreement.validFrom), + validUntil: formatDateForForm(agreement.validUntil), + }, + }); + + const [mutate] = useMutationWithToasts(updateBusinessAssociateAgreementMutation, { + successMessage: __("Business Associate Agreement updated successfully"), + errorMessage: __("Failed to update Business Associate Agreement"), + }); + + const onSubmit = handleSubmit(async (data) => { + const formatDatetime = (dateString?: string) => { + if (!dateString) return null; + return `${dateString}T00:00:00Z`; + }; + + await mutate({ + variables: { + input: { + vendorId, + validFrom: formatDatetime(data.validFrom), + validUntil: formatDatetime(data.validUntil), + }, + }, + }); + + onSuccess?.(); + ref.current?.close(); + }); + + const handleClose = () => { + reset(); + }; + + return ( + +
+ +
+ + + + + + +
+
+ + + + +
+
+ ); +} diff --git a/apps/console/src/pages/organizations/vendors/dialogs/UploadBusinessAssociateAgreementDialog.tsx b/apps/console/src/pages/organizations/vendors/dialogs/UploadBusinessAssociateAgreementDialog.tsx new file mode 100644 index 000000000..894e7807a --- /dev/null +++ b/apps/console/src/pages/organizations/vendors/dialogs/UploadBusinessAssociateAgreementDialog.tsx @@ -0,0 +1,178 @@ +import { + Button, + Dialog, + DialogContent, + DialogFooter, + Dropzone, + Field, + Spinner, + Input, + useDialogRef, +} from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import { graphql } from "react-relay"; +import { z } from "zod"; +import { useFormWithSchema } from "/hooks/useFormWithSchema"; +import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; +import { useState } from "react"; + +const uploadBusinessAssociateAgreementMutation = graphql` + mutation UploadBusinessAssociateAgreementDialogMutation( + $input: UploadVendorBusinessAssociateAgreementInput! + ) { + uploadVendorBusinessAssociateAgreement(input: $input) { + vendorBusinessAssociateAgreement { + id + fileName + fileUrl + validFrom + validUntil + createdAt + } + } + } +`; + +const schema = z.object({ + fileName: z.string().min(1, "File name is required"), + validFrom: z.string().optional(), + validUntil: z.string().optional(), +}); + +type Props = { + children: React.ReactNode; + vendorId: string; + onSuccess?: () => void; +}; + +export function UploadBusinessAssociateAgreementDialog({ + children, + vendorId, + onSuccess, +}: Props) { + const { __ } = useTranslate(); + const [uploadedFile, setUploadedFile] = useState(null); + const ref = useDialogRef(); + + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + reset, + setValue, + } = useFormWithSchema(schema, { + defaultValues: { + fileName: "", + validFrom: "", + validUntil: "", + }, + }); + + const [mutate] = useMutationWithToasts(uploadBusinessAssociateAgreementMutation, { + successMessage: __("Business Associate Agreement uploaded successfully"), + errorMessage: __("Failed to upload Business Associate Agreement"), + }); + + const handleDrop = (files: File[]) => { + if (files.length > 0) { + const file = files[0]; + setUploadedFile(file); + setValue("fileName", file.name); + } + }; + + const onSubmit = handleSubmit(async (data) => { + if (!uploadedFile) { + return; + } + + const formatDatetime = (dateString?: string) => { + if (!dateString) return null; + return `${dateString}T00:00:00Z`; + }; + + await mutate({ + variables: { + input: { + vendorId, + fileName: data.fileName, + validFrom: formatDatetime(data.validFrom), + validUntil: formatDatetime(data.validUntil), + file: null, + }, + }, + uploadables: { + "input.file": uploadedFile, + }, + }); + + reset(); + setUploadedFile(null); + onSuccess?.(); + ref.current?.close(); + }); + + const handleClose = () => { + reset(); + setUploadedFile(null); + }; + + return ( + +
+ + + + {uploadedFile && ( +
+

{__("Selected file")}:

+

{uploadedFile.name}

+
+ )} + + + +
+ + + + + + +
+
+ + + + +
+
+ ); +} diff --git a/apps/console/src/pages/organizations/vendors/dialogs/__generated__/DeleteBusinessAssociateAgreementDialogMutation.graphql.ts b/apps/console/src/pages/organizations/vendors/dialogs/__generated__/DeleteBusinessAssociateAgreementDialogMutation.graphql.ts new file mode 100644 index 000000000..974664c2d --- /dev/null +++ b/apps/console/src/pages/organizations/vendors/dialogs/__generated__/DeleteBusinessAssociateAgreementDialogMutation.graphql.ts @@ -0,0 +1,92 @@ +/** + * @generated SignedSource<<26343418dbdd4baf63f2846470e2d3ab>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DeleteVendorBusinessAssociateAgreementInput = { + vendorId: string; +}; +export type DeleteBusinessAssociateAgreementDialogMutation$variables = { + input: DeleteVendorBusinessAssociateAgreementInput; +}; +export type DeleteBusinessAssociateAgreementDialogMutation$data = { + readonly deleteVendorBusinessAssociateAgreement: { + readonly deletedVendorId: string; + }; +}; +export type DeleteBusinessAssociateAgreementDialogMutation = { + response: DeleteBusinessAssociateAgreementDialogMutation$data; + variables: DeleteBusinessAssociateAgreementDialogMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "DeleteVendorBusinessAssociateAgreementPayload", + "kind": "LinkedField", + "name": "deleteVendorBusinessAssociateAgreement", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deletedVendorId", + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "DeleteBusinessAssociateAgreementDialogMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "DeleteBusinessAssociateAgreementDialogMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "c8aa903ee410ee9e591c82f7f75e044c", + "id": null, + "metadata": {}, + "name": "DeleteBusinessAssociateAgreementDialogMutation", + "operationKind": "mutation", + "text": "mutation DeleteBusinessAssociateAgreementDialogMutation(\n $input: DeleteVendorBusinessAssociateAgreementInput!\n) {\n deleteVendorBusinessAssociateAgreement(input: $input) {\n deletedVendorId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "39106d060e4e1da4eaa78a8205fe7cf8"; + +export default node; diff --git a/apps/console/src/pages/organizations/vendors/dialogs/__generated__/EditBusinessAssociateAgreementDialogMutation.graphql.ts b/apps/console/src/pages/organizations/vendors/dialogs/__generated__/EditBusinessAssociateAgreementDialogMutation.graphql.ts new file mode 100644 index 000000000..f1c56c6b1 --- /dev/null +++ b/apps/console/src/pages/organizations/vendors/dialogs/__generated__/EditBusinessAssociateAgreementDialogMutation.graphql.ts @@ -0,0 +1,139 @@ +/** + * @generated SignedSource<<8d5961820ccfa705a899582a9a8ca953>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type UpdateVendorBusinessAssociateAgreementInput = { + validFrom?: any | null | undefined; + validUntil?: any | null | undefined; + vendorId: string; +}; +export type EditBusinessAssociateAgreementDialogMutation$variables = { + input: UpdateVendorBusinessAssociateAgreementInput; +}; +export type EditBusinessAssociateAgreementDialogMutation$data = { + readonly updateVendorBusinessAssociateAgreement: { + readonly vendorBusinessAssociateAgreement: { + readonly createdAt: any; + readonly fileUrl: string; + readonly id: string; + readonly validFrom: any | null | undefined; + readonly validUntil: any | null | undefined; + }; + }; +}; +export type EditBusinessAssociateAgreementDialogMutation = { + response: EditBusinessAssociateAgreementDialogMutation$data; + variables: EditBusinessAssociateAgreementDialogMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "UpdateVendorBusinessAssociateAgreementPayload", + "kind": "LinkedField", + "name": "updateVendorBusinessAssociateAgreement", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "VendorBusinessAssociateAgreement", + "kind": "LinkedField", + "name": "vendorBusinessAssociateAgreement", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fileUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "validFrom", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "validUntil", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "EditBusinessAssociateAgreementDialogMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "EditBusinessAssociateAgreementDialogMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "2e50e54d255a9bea661d82188913ed9c", + "id": null, + "metadata": {}, + "name": "EditBusinessAssociateAgreementDialogMutation", + "operationKind": "mutation", + "text": "mutation EditBusinessAssociateAgreementDialogMutation(\n $input: UpdateVendorBusinessAssociateAgreementInput!\n) {\n updateVendorBusinessAssociateAgreement(input: $input) {\n vendorBusinessAssociateAgreement {\n id\n fileUrl\n validFrom\n validUntil\n createdAt\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "10b614dbbdee430ed650b94051be4bbc"; + +export default node; diff --git a/apps/console/src/pages/organizations/vendors/dialogs/__generated__/UploadBusinessAssociateAgreementDialogMutation.graphql.ts b/apps/console/src/pages/organizations/vendors/dialogs/__generated__/UploadBusinessAssociateAgreementDialogMutation.graphql.ts new file mode 100644 index 000000000..33198ffec --- /dev/null +++ b/apps/console/src/pages/organizations/vendors/dialogs/__generated__/UploadBusinessAssociateAgreementDialogMutation.graphql.ts @@ -0,0 +1,149 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type UploadVendorBusinessAssociateAgreementInput = { + file: any; + fileName: string; + validFrom?: any | null | undefined; + validUntil?: any | null | undefined; + vendorId: string; +}; +export type UploadBusinessAssociateAgreementDialogMutation$variables = { + input: UploadVendorBusinessAssociateAgreementInput; +}; +export type UploadBusinessAssociateAgreementDialogMutation$data = { + readonly uploadVendorBusinessAssociateAgreement: { + readonly vendorBusinessAssociateAgreement: { + readonly createdAt: any; + readonly fileName: string; + readonly fileUrl: string; + readonly id: string; + readonly validFrom: any | null | undefined; + readonly validUntil: any | null | undefined; + }; + }; +}; +export type UploadBusinessAssociateAgreementDialogMutation = { + response: UploadBusinessAssociateAgreementDialogMutation$data; + variables: UploadBusinessAssociateAgreementDialogMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "UploadVendorBusinessAssociateAgreementPayload", + "kind": "LinkedField", + "name": "uploadVendorBusinessAssociateAgreement", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "VendorBusinessAssociateAgreement", + "kind": "LinkedField", + "name": "vendorBusinessAssociateAgreement", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fileName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fileUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "validFrom", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "validUntil", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "UploadBusinessAssociateAgreementDialogMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "UploadBusinessAssociateAgreementDialogMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "bb7a0f67773da5b3916d08fdeaa2dc8d", + "id": null, + "metadata": {}, + "name": "UploadBusinessAssociateAgreementDialogMutation", + "operationKind": "mutation", + "text": "mutation UploadBusinessAssociateAgreementDialogMutation(\n $input: UploadVendorBusinessAssociateAgreementInput!\n) {\n uploadVendorBusinessAssociateAgreement(input: $input) {\n vendorBusinessAssociateAgreement {\n id\n fileName\n fileUrl\n validFrom\n validUntil\n createdAt\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "e3fce209e1a4006a31996903e4c6cbae"; + +export default node; diff --git a/apps/console/src/pages/organizations/vendors/tabs/VendorOverviewTab.tsx b/apps/console/src/pages/organizations/vendors/tabs/VendorOverviewTab.tsx index 4ff9cc113..a1b1283a1 100644 --- a/apps/console/src/pages/organizations/vendors/tabs/VendorOverviewTab.tsx +++ b/apps/console/src/pages/organizations/vendors/tabs/VendorOverviewTab.tsx @@ -1,19 +1,44 @@ import { useTranslate } from "@probo/i18n"; import { useVendorForm } from "/hooks/forms/useVendorForm"; -import type { useVendorFormFragment$key } from "/hooks/forms/__generated__/useVendorFormFragment.graphql"; import { useOutletContext } from "react-router"; -import { Button, Card, Field, Input } from "@probo/ui"; +import { Button, Card, Field, Input, IconPlusLarge, IconTrashCan, IconPencil } from "@probo/ui"; import { PeopleSelectField } from "/components/form/PeopleSelectField"; import { useOrganizationId } from "/hooks/useOrganizationId"; import { useMemo } from "react"; import { usePageTitle } from "@probo/hooks"; +import { downloadFile } from "@probo/helpers"; +import { useFragment, graphql } from "react-relay"; +import { UploadBusinessAssociateAgreementDialog } from "../dialogs/UploadBusinessAssociateAgreementDialog"; +import { DeleteBusinessAssociateAgreementDialog } from "../dialogs/DeleteBusinessAssociateAgreementDialog"; +import { EditBusinessAssociateAgreementDialog } from "../dialogs/EditBusinessAssociateAgreementDialog"; +import type { useVendorFormFragment$key } from "/hooks/forms/__generated__/useVendorFormFragment.graphql"; +import type { VendorOverviewTabBusinessAssociateAgreementFragment$key } from "./__generated__/VendorOverviewTabBusinessAssociateAgreementFragment.graphql"; + +const vendorBusinessAssociateAgreementFragment = graphql` + fragment VendorOverviewTabBusinessAssociateAgreementFragment on Vendor { + businessAssociateAgreement { + id + fileName + fileUrl + validFrom + validUntil + createdAt + } + } +`; export default function VendorOverviewTab() { const { vendor } = useOutletContext<{ - vendor: useVendorFormFragment$key & { name: string }; + vendor: useVendorFormFragment$key & { id: string; name: string }; }>(); + + const { vendor: vendorForBAA } = useOutletContext<{ + vendor: VendorOverviewTabBusinessAssociateAgreementFragment$key; + }>(); + const { __ } = useTranslate(); const organizationId = useOrganizationId(); + const { control, register, @@ -21,6 +46,12 @@ export default function VendorOverviewTab() { formState: { errors, isSubmitting }, } = useVendorForm(vendor); + const vendorWithBAA = useFragment( + vendorBusinessAssociateAgreementFragment, + vendorForBAA + ); + const businessAssociateAgreement = vendorWithBAA.businessAssociateAgreement; + const urls = useMemo( () => [ @@ -132,6 +163,73 @@ export default function VendorOverviewTab() { + {/* Data agreements */} +
+

{__("Data agreements")}

+ +
+
+

+ {__("Business Associate Agreement")} +

+

+ {businessAssociateAgreement ? businessAssociateAgreement.fileName : __("No business associate agreement available")} +

+ {(businessAssociateAgreement?.validFrom || businessAssociateAgreement?.validUntil) && ( +

+ {__("Valid")} + {businessAssociateAgreement.validFrom && + ` ${__("from")} ${new Date(businessAssociateAgreement.validFrom).toLocaleDateString()}` + } + {businessAssociateAgreement.validUntil && + ` ${__("until")} ${new Date(businessAssociateAgreement.validUntil).toLocaleDateString()}` + } +

+ )} +
+
+ {businessAssociateAgreement ? ( + <> + + window.location.reload()} + > + + + )} +
+
+
+
+ {/* Submit */}