diff --git a/apps/console/src/App.tsx b/apps/console/src/App.tsx index 9e735d9a8..0225537d1 100644 --- a/apps/console/src/App.tsx +++ b/apps/console/src/App.tsx @@ -13,6 +13,7 @@ import AuthLayout from "./layouts/AuthLayout"; import { RelayEnvironment } from "./RelayEnvironment"; import { AuthenticationRoutes } from "./pages/authentication/Routes"; import { OrganizationsRoutes } from "./pages/organizations/Routes"; +import SigningRequestsPage from "./pages/SigningRequestsPage"; posthog.init(process.env.POSTHOG_KEY!, { api_host: process.env.POSTHOG_HOST, @@ -53,6 +54,11 @@ function App() { element={} /> + } + /> + (null); + const [signingData, setSigningData] = useState(null); + const [currentDocIndex, setCurrentDocIndex] = useState(0); + + // Fetch documents to sign using the token + 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(buildEndpoint("/api/signing-requests"), { + method: "GET", + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch signing documents"); + } + + const data: SigningResponse = await response.json(); + setSigningData(data); + } catch (err) { + setError(err instanceof Error ? err.message : "An unknown error occurred"); + } finally { + setLoading(false); + } + } + + fetchDocuments(); + }, [token]); + + // Handle document signing + const handleSignDocument = async () => { + if (!signingData || !token) return; + + const docToSign = signingData.documents[currentDocIndex]; + + try { + const response = await fetch(buildEndpoint(`/api/signing-requests/${docToSign.id}/sign`), { + method: "POST", + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to sign document"); + } + + // Update local state + const updatedDocs = [...signingData.documents]; + updatedDocs[currentDocIndex] = { + ...updatedDocs[currentDocIndex], + signed: true, + }; + + setSigningData({ + ...signingData, + documents: updatedDocs, + }); + + // Move to next document if available + if (currentDocIndex < updatedDocs.length - 1) { + setCurrentDocIndex(currentDocIndex + 1); + } + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to sign document"); + } + }; + + // Handle going to next document + const handleNextDocument = () => { + if (signingData && currentDocIndex < signingData.documents.length - 1) { + setCurrentDocIndex(currentDocIndex + 1); + } + }; + + // Calculate progress + 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; + }; + + // Loading state + if (loading) { + return ( +
+ + + Loading Signing Requests + Please wait while we fetch your documents... + + +
+ ); + } + + // Error state + if (error) { + return ( +
+ + + Error + {error} + + + + + +
+ ); + } + + // No documents or missing data + if (!signingData || signingData.documents.length === 0) { + return ( +
+ + + No Documents to Sign + There are no documents requiring your signature at this time. + + +
+ ); + } + + // Current document + const currentDoc = signingData.documents[currentDocIndex]; + const isLastDocument = currentDocIndex === signingData.documents.length - 1; + const allSigned = getSignedCount() === signingData.documents.length; + + return ( +
+ + Document Signing - Probo + + +
+

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 +
+ )} +
+ +
+ ); +} \ No newline at end of file diff --git a/apps/console/src/pages/organizations/policies/PolicyListView.tsx b/apps/console/src/pages/organizations/policies/PolicyListView.tsx index 20a06ef35..670d69b71 100644 --- a/apps/console/src/pages/organizations/policies/PolicyListView.tsx +++ b/apps/console/src/pages/organizations/policies/PolicyListView.tsx @@ -29,6 +29,7 @@ import { format } from "date-fns"; import type { PolicyListViewQuery, PolicyListViewQuery$data } from "./__generated__/PolicyListViewQuery.graphql"; import type { PolicyListViewDeleteMutation } from "./__generated__/PolicyListViewDeleteMutation.graphql"; import type { PolicyListViewCreateMutation } from "./__generated__/PolicyListViewCreateMutation.graphql"; +import type { PolicyListViewSendSigningNotificationsMutation } from "./__generated__/PolicyListViewSendSigningNotificationsMutation.graphql"; import { PageTemplate } from "@/components/PageTemplate"; import { PolicyListViewSkeleton } from "./PolicyListPage"; import { @@ -117,6 +118,13 @@ const createPolicyMutation = graphql` } `; +const sendSigningNotificationsMutation = graphql` + mutation PolicyListViewSendSigningNotificationsMutation($input: SendSigningNotificationsInput!) { + sendSigningNotifications(input: $input) { + success + } + } +`; function PolicyTableRow({ policy, organizationId, @@ -360,7 +368,10 @@ function CreatePolicyModal({ className={`text-4xl leading-tight font-bold outline-none focus:outline-none ${!title ? 'text-gray-400' : 'text-black'}`} contentEditable suppressContentEditableWarning - onInput={(e) => setTitle(e.currentTarget.textContent || "")} + onInput={(e) => { + const newText = e.currentTarget.textContent || ""; + setTitle(newText); + }} style={{ WebkitTapHighlightColor: 'transparent' }} onClick={(e) => { if (!title) { @@ -368,12 +379,22 @@ function CreatePolicyModal({ } }} onFocus={(e) => { - if (!title) { + if (e.currentTarget.textContent === "Enter policy title...") { e.currentTarget.textContent = ''; } }} + onBlur={(e) => { + if (!e.currentTarget.textContent?.trim()) { + e.currentTarget.textContent = "Enter policy title..."; + setTitle(""); + } + }} + ref={(el) => { + if (el && !el.textContent) { + el.textContent = title || "Enter policy title..."; + } + }} > - {title || "Enter policy title..."}