diff --git a/apps/console2/src/components/documentSigning/ProgressBar.tsx b/apps/console2/src/components/documentSigning/ProgressBar.tsx new file mode 100644 index 000000000..a9253b375 --- /dev/null +++ b/apps/console2/src/components/documentSigning/ProgressBar.tsx @@ -0,0 +1,15 @@ +interface ProgressBarProps { + value: number; + className?: string; +} + +export function ProgressBar({ value, className }: ProgressBarProps) { + return ( +
+
+
+ ); +} diff --git a/apps/console2/src/hooks/graph/DocumentGraph.ts b/apps/console2/src/hooks/graph/DocumentGraph.ts index 9e10aaa48..9e42ba868 100644 --- a/apps/console2/src/hooks/graph/DocumentGraph.ts +++ b/apps/console2/src/hooks/graph/DocumentGraph.ts @@ -2,6 +2,7 @@ import { useTranslate } from "@probo/i18n"; import { graphql } from "relay-runtime"; import { useMutationWithToasts } from "../useMutationWithToasts"; import type { DocumentGraphDeleteMutation } from "./__generated__/DocumentGraphDeleteMutation.graphql"; +import type { DocumentGraphSendSigningNotificationsMutation } from "./__generated__/DocumentGraphSendSigningNotificationsMutation.graphql"; export const documentsQuery = graphql` query DocumentGraphListQuery($organizationId: ID!) { @@ -37,6 +38,28 @@ export function useDeleteDocumentMutation() { ); } +const sendSigningNotificationsMutation = graphql` + mutation DocumentGraphSendSigningNotificationsMutation( + $input: SendSigningNotificationsInput! + ) { + sendSigningNotifications(input: $input) { + success + } + } +`; + +export function useSendSigningNotificationsMutation() { + const { __ } = useTranslate(); + + return useMutationWithToasts( + sendSigningNotificationsMutation, + { + successMessage: __("Signing notifications sent successfully."), + errorMessage: __("Failed to send signing notifications. Please try again."), + } + ); +} + export const documentNodeQuery = graphql` query DocumentGraphNodeQuery($documentId: ID!) { node(id: $documentId) { diff --git a/apps/console2/src/hooks/graph/__generated__/AssetGraphListQuery.graphql.ts b/apps/console2/src/hooks/graph/__generated__/AssetGraphListQuery.graphql.ts index d646a1c32..6a61fc42d 100644 --- a/apps/console2/src/hooks/graph/__generated__/AssetGraphListQuery.graphql.ts +++ b/apps/console2/src/hooks/graph/__generated__/AssetGraphListQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<00b3c915ccd04831f9161442e27b635e>> * @lightSyntaxTransform * @nogrep */ @@ -42,14 +42,6 @@ export type AssetGraphListQuery$data = { }; }>; }; - readonly peoples?: { - readonly edges: ReadonlyArray<{ - readonly node: { - readonly fullName: string; - readonly id: string; - }; - }>; - }; }; }; export type AssetGraphListQuery = { @@ -233,42 +225,7 @@ v15 = [ "name": "first", "value": 100 } -], -v16 = { - "alias": null, - "args": (v15/*: any*/), - "concreteType": "PeopleConnection", - "kind": "LinkedField", - "name": "peoples", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "PeopleEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "People", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v2/*: any*/), - (v8/*: any*/) - ], - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": "peoples(first:100)" -}; +]; return { "fragment": { "argumentDefinitions": (v0/*: any*/), @@ -343,8 +300,7 @@ return { (v14/*: any*/) ], "storageKey": null - }, - (v16/*: any*/) + } ], "type": "Organization", "abstractKey": null @@ -440,8 +396,7 @@ return { "key": "AssetsPage_assets", "kind": "LinkedHandle", "name": "assets" - }, - (v16/*: any*/) + } ], "type": "Organization", "abstractKey": null @@ -453,7 +408,7 @@ return { ] }, "params": { - "cacheID": "2b452fda67547e63fb04fd4c6ae3ec7e", + "cacheID": "b070f08982995825d2695f833f773f5e", "id": null, "metadata": { "connection": [ @@ -470,11 +425,11 @@ return { }, "name": "AssetGraphListQuery", "operationKind": "query", - "text": "query AssetGraphListQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n assets(first: 100) {\n edges {\n node {\n id\n name\n amount\n criticity\n assetType\n dataTypesStored\n owner {\n fullName\n id\n }\n vendors(first: 50) {\n edges {\n node {\n id\n name\n websiteUrl\n }\n }\n }\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n peoples(first: 100) {\n edges {\n node {\n id\n fullName\n }\n }\n }\n }\n id\n }\n}\n" + "text": "query AssetGraphListQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n assets(first: 100) {\n edges {\n node {\n id\n name\n amount\n criticity\n assetType\n dataTypesStored\n owner {\n fullName\n id\n }\n vendors(first: 50) {\n edges {\n node {\n id\n name\n websiteUrl\n }\n }\n }\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n" } }; })(); -(node as any).hash = "63f6a6df2bdbcd5539e28f892e54c686"; +(node as any).hash = "79e2cf57067fabd4b124e6c0b47220dd"; export default node; diff --git a/apps/console2/src/hooks/graph/__generated__/DocumentGraphSendSigningNotificationsMutation.graphql.ts b/apps/console2/src/hooks/graph/__generated__/DocumentGraphSendSigningNotificationsMutation.graphql.ts new file mode 100644 index 000000000..b5547743b --- /dev/null +++ b/apps/console2/src/hooks/graph/__generated__/DocumentGraphSendSigningNotificationsMutation.graphql.ts @@ -0,0 +1,92 @@ +/** + * @generated SignedSource<<242a8628b9203b02dccea5e311141ec4>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type SendSigningNotificationsInput = { + organizationId: string; +}; +export type DocumentGraphSendSigningNotificationsMutation$variables = { + input: SendSigningNotificationsInput; +}; +export type DocumentGraphSendSigningNotificationsMutation$data = { + readonly sendSigningNotifications: { + readonly success: boolean; + }; +}; +export type DocumentGraphSendSigningNotificationsMutation = { + response: DocumentGraphSendSigningNotificationsMutation$data; + variables: DocumentGraphSendSigningNotificationsMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "SendSigningNotificationsPayload", + "kind": "LinkedField", + "name": "sendSigningNotifications", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "success", + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "DocumentGraphSendSigningNotificationsMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "DocumentGraphSendSigningNotificationsMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "111167f828fa6eaad3beb44c43e43f06", + "id": null, + "metadata": {}, + "name": "DocumentGraphSendSigningNotificationsMutation", + "operationKind": "mutation", + "text": "mutation DocumentGraphSendSigningNotificationsMutation(\n $input: SendSigningNotificationsInput!\n) {\n sendSigningNotifications(input: $input) {\n success\n }\n}\n" + } +}; +})(); + +(node as any).hash = "eaf2fba7629e202537cfc7bd9b5c9586"; + +export default node; diff --git a/apps/console2/src/pages/DocumentSigningRequestsPage.tsx b/apps/console2/src/pages/DocumentSigningRequestsPage.tsx new file mode 100644 index 000000000..a986c8757 --- /dev/null +++ b/apps/console2/src/pages/DocumentSigningRequestsPage.tsx @@ -0,0 +1,264 @@ +import { useEffect, useState } from "react"; +import { useSearchParams } from "react-router"; +import { useTranslate } from "@probo/i18n"; +import { + Button, + Card, + Markdown, + IconCheckmark1, + IconCircleProgress +} from "@probo/ui"; +import { ProgressBar } from "../components/documentSigning/ProgressBar"; + +type Document = { + document_version_id: string; + title: string; + content: string; + signed?: boolean; +}; + +type DocumentSigningResponse = { + documents: Document[]; + requesterName: string; + requesterOrganization: string; +}; + +export default function DocumentSigningRequestsPage() { + const { __ } = useTranslate(); + const [searchParams] = useSearchParams(); + const token = searchParams.get("token"); + + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [signingData, setSigningData] = useState(null); + const [currentDocIndex, setCurrentDocIndex] = useState(0); + + useEffect(() => { + if (!token) { + setError(__("Missing signing token. Please check your URL and try again.")); + setLoading(false); + return; + } + + async function fetchDocuments() { + try { + const response = await fetch( + `${import.meta.env.VITE_API_URL}/api/console/v1/documents/signing-requests`, + { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }, + ); + + if (!response.ok) { + throw new Error(__("Failed to fetch signing documents")); + } + + const documents: Document[] = await response.json(); + + const enhancedDocuments = documents.map((doc) => ({ + ...doc, + signed: false, + })); + + setSigningData({ + documents: enhancedDocuments, + requesterName: "Requester", + requesterOrganization: "Organization", + }); + } catch (err) { + setError( + err instanceof Error ? err.message : __("An unknown error occurred"), + ); + } finally { + setLoading(false); + } + } + + fetchDocuments(); + }, [token, __]); + + const handleSignDocument = async () => { + if (!signingData || !token) return; + + const docToSign = signingData.documents[currentDocIndex]; + + try { + const response = await fetch( + `${import.meta.env.VITE_API_URL}/api/console/v1/documents/signing-requests/${docToSign.document_version_id}/sign`, + { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }, + ); + + if (!response.ok) { + throw new Error(__("Failed to sign document")); + } + + const updatedDocs = [...signingData.documents]; + updatedDocs[currentDocIndex] = { + ...updatedDocs[currentDocIndex], + signed: true, + }; + + setSigningData({ + ...signingData, + documents: updatedDocs, + }); + + if (currentDocIndex < updatedDocs.length - 1) { + setCurrentDocIndex(currentDocIndex + 1); + } + } catch (err) { + setError(err instanceof Error ? err.message : __("Failed to sign document")); + } + }; + + const handleNextDocument = () => { + if (signingData && currentDocIndex < signingData.documents.length - 1) { + setCurrentDocIndex(currentDocIndex + 1); + } + }; + + const getSignedCount = () => { + if (!signingData) return 0; + return signingData.documents.filter((doc) => doc.signed).length; + }; + + const getProgressPercentage = () => { + if (!signingData || signingData.documents.length === 0) return 0; + return (getSignedCount() / signingData.documents.length) * 100; + }; + + if (loading) { + return ( + <> + {__("Loading Signing Requests")} +
+ +
+ +
+

{__("Loading Signing Requests")}

+

+ {__("Please wait while we fetch your documents...")} +

+
+
+
+
+ + ); + } + + if (error) { + return ( + <> + {__("Error")} +
+ +

{__("Error")}

+

{error}

+ +
+
+ + ); + } + + if (!signingData || signingData.documents.length === 0) { + return ( + <> + {__("No Documents to Sign")} +
+ +

{__("No Documents to Sign")}

+

+ {__("There are no documents requiring your signature at this time.")} +

+
+
+ + ); + } + + const currentDoc = signingData.documents[currentDocIndex]; + const isLastDocument = currentDocIndex === signingData.documents.length - 1; + const allSigned = getSignedCount() === signingData.documents.length; + + return ( + <> + {__("Document Signing")} +
+
+

{__("Document Signing Request")}

+

+ {__("From")} {signingData.requesterName} {__("at")} {signingData.requesterOrganization} +

+ +
+
+ + {getSignedCount()} {__("of")} {signingData.documents.length} {__("documents signed")} + + + {Math.round(getProgressPercentage())}% + +
+ +
+
+ + +
+
+

{currentDoc.title}

+

+ {__("Document")} {currentDocIndex + 1} {__("of")} {signingData.documents.length} +

+
+ +
+ +
+ +
+ {currentDoc.signed ? ( +
+
+ + {__("Signed")} +
+ {!isLastDocument && ( + + )} +
+ ) : ( + + )} + + {allSigned && ( +
+ {__("All documents have been signed")} +
+ )} +
+
+
+
+ + ); +} diff --git a/apps/console2/src/pages/organizations/documents/DocumentsPage.tsx b/apps/console2/src/pages/organizations/documents/DocumentsPage.tsx index 045a864b4..ff4e72a34 100644 --- a/apps/console2/src/pages/organizations/documents/DocumentsPage.tsx +++ b/apps/console2/src/pages/organizations/documents/DocumentsPage.tsx @@ -15,6 +15,7 @@ import { useConfirm, ActionDropdown, DropdownItem, + IconBell2, } from "@probo/ui"; import { useFragment, @@ -26,6 +27,7 @@ import type { DocumentGraphListQuery } from "/hooks/graph/__generated__/Document import { documentsQuery, useDeleteDocumentMutation, + useSendSigningNotificationsMutation, } from "/hooks/graph/DocumentGraph"; import type { DocumentsPageListFragment$key } from "./__generated__/DocumentsPageListFragment.graphql"; import { usePageTitle } from "@probo/hooks"; @@ -65,18 +67,37 @@ export default function DocumentsPage(props: Props) { const documents = data.documents.edges.map((edge) => edge.node); const connectionId = data.documents.__id; + const [sendSigningNotifications] = useSendSigningNotificationsMutation(); + usePageTitle(__("Documents")); + const handleSendSigningNotifications = () => { + sendSigningNotifications({ + variables: { + input: { organizationId: organization.id }, + }, + }); + }; + return (
- {__("New document")}} - /> +
+ + {__("New document")}} + /> +
diff --git a/apps/console2/src/routes.tsx b/apps/console2/src/routes.tsx index b4dbe8bd6..dce90bcc0 100644 --- a/apps/console2/src/routes.tsx +++ b/apps/console2/src/routes.tsx @@ -72,6 +72,10 @@ const routes = [ () => import("./pages/organizations/NewOrganizationPage") ), }, + { + path: "documents/signing-requests", + Component: lazy(() => import("./pages/DocumentSigningRequestsPage.tsx")), + }, ], }, {