-
-
{currentDoc.title}
-
- {__("Document")} {currentDocIndex + 1} {__("of")}{" "}
- {signingData.documents.length}
+
+
+
+
+
+ {sprintf(__("%s requests your signature"), signingData.organizationName)}
+
+ {allSigned ? (
+
+ {__(
+ "You have successfully signed all documents. You can now close this page."
+ )}
-
+ ) : (
+ <>
+
+ {__("Please review and sign the following documents:")}
+
+
+
+ {(() => {
+ const renderDocumentItem = (doc: Document, index: number) => (
+
+
+ {doc.signed ? (
+
+ ) : (
+
+ {index + 1}
+
+ )}
+
+
+
+
+ {doc.signed
+ ? __("Signed")
+ : index === currentDocIndex
+ ? __("In review")
+ : __("Waiting signature")}
+
+
+
+ );
-
-
-
+ const totalDocs = signingData.documents.length;
-
- {currentDoc.signed ? (
-
-
-
- {__("Signed")}
+ if (totalDocs <= 4) {
+ return signingData.documents.map((doc, index) =>
+ renderDocumentItem(doc, index)
+ );
+ }
+
+ if (showAllDocuments) {
+ return (
+ <>
+ {signingData.documents.map((doc, index) =>
+ renderDocumentItem(doc, index)
+ )}
+
+ >
+ );
+ }
+
+ // Always show current document in collapsed view with two "show more" buttons
+ const firstDoc = signingData.documents[0];
+ const currentIsFirst = currentDocIndex === 0;
+ const currentIsLast = currentDocIndex === totalDocs - 1;
+
+ // Calculate hidden docs before and after current
+ const hiddenBeforeCurrent = currentIsFirst ? 0 : currentDocIndex - 1;
+ const hiddenAfterCurrent = currentIsLast ? 0 : totalDocs - currentDocIndex - 2;
+
+ return (
+ <>
+ {/* First document */}
+ {renderDocumentItem(firstDoc, 0)}
+
+ {/* Show more button for documents BEFORE current (signed documents) */}
+ {hiddenBeforeCurrent > 0 && (
+
+ )}
+
+ {/* Current document (if not first) */}
+ {!currentIsFirst && renderDocumentItem(currentDoc, currentDocIndex)}
+
+ {/* Show more button for documents AFTER current (upcoming documents) */}
+ {hiddenAfterCurrent > 0 && (
+
+ )}
+ >
+ );
+ })()}
- {!isLastDocument && (
-
+
+
+ {__(
+ "Please review the document carefully before signing."
)}
-
- ) : (
-
- )}
-
+ >
+ Powered by
+
-
+ {isDesktop && (
+
+ )}
+
>
);
diff --git a/pkg/probo/document_service.go b/pkg/probo/document_service.go
index 4aab33a74..815402ab3 100644
--- a/pkg/probo/document_service.go
+++ b/pkg/probo/document_service.go
@@ -377,15 +377,24 @@ func (s *DocumentService) ListSigningRequests(
SELECT
p.title,
pv.content,
- pv.id AS document_version_id
+ pv.id AS document_version_id,
+ o.name AS organization_name
FROM
documents p
INNER JOIN document_versions pv ON pv.document_id = p.id
INNER JOIN document_version_signatures pvs ON pvs.document_version_id = pv.id
+ INNER JOIN organizations o ON o.id = p.organization_id
WHERE
p.tenant_id = $1
AND pvs.signed_by = $2
AND pvs.signed_at IS NULL
+ AND pv.status = 'PUBLISHED'
+ AND pv.version_number = (
+ SELECT MAX(pv2.version_number)
+ FROM document_versions pv2
+ WHERE pv2.document_id = pv.document_id
+ AND pv2.status = 'PUBLISHED'
+ )
`
var results []map[string]any
diff --git a/pkg/server/api/console/v1/resolver.go b/pkg/server/api/console/v1/resolver.go
index 7deb3905c..48b769313 100644
--- a/pkg/server/api/console/v1/resolver.go
+++ b/pkg/server/api/console/v1/resolver.go
@@ -122,6 +122,54 @@ func NewMux(
},
)
+ r.Get(
+ "/documents/signing-requests/{document_version_id}/pdf",
+ func(w http.ResponseWriter, r *http.Request) {
+ token := r.URL.Query().Get("token")
+ if token == "" {
+ http.Error(w, "token is required", http.StatusUnauthorized)
+ return
+ }
+
+ data, err := statelesstoken.ValidateToken[probo.SigningRequestData](authCfg.CookieSecret, probo.TokenTypeSigningRequest, token)
+ if err != nil {
+ http.Error(w, "invalid token", http.StatusUnauthorized)
+ return
+ }
+
+ documentVersionID, err := gid.ParseGID(chi.URLParam(r, "document_version_id"))
+ if err != nil {
+ http.Error(w, "invalid document version id", http.StatusBadRequest)
+ return
+ }
+
+ svc := proboSvc.WithTenant(data.Data.OrganizationID.TenantID())
+
+ // Get the people to get their email for watermark
+ people, err := svc.Peoples.Get(r.Context(), data.Data.PeopleID)
+ if err != nil {
+ http.Error(w, "failed to get user", http.StatusInternalServerError)
+ return
+ }
+
+ // Generate PDF with watermark
+ pdfData, err := svc.Documents.ExportPDF(r.Context(), documentVersionID, probo.ExportPDFOptions{
+ WithWatermark: true,
+ WatermarkEmail: &people.PrimaryEmailAddress,
+ WithSignatures: false,
+ })
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/pdf")
+ w.Header().Set("Content-Disposition", "inline; filename=\"document.pdf\"")
+ w.WriteHeader(http.StatusOK)
+ w.Write(pdfData)
+ },
+ )
+
r.Post(
"/documents/signing-requests/{document_version_id}/sign",
func(w http.ResponseWriter, r *http.Request) {