feat(documents): detail page with signers, watchers, activity, actions

Replaces the PR4 stub at /documents/[id] with the full Phase A detail
view: gradient header strip, status-aware action bar (Cancel /
Download / Email signatories), per-signer remind + copy-link, watcher
list with remove, and activity timeline. Adds the supporting endpoints
(cancel, compose-completion-email, watchers GET/POST/DELETE) and
listDocumentWatchers / addDocumentWatcher / removeDocumentWatcher
service helpers. The document GET now serves the aggregator shape
when ?detail=true.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-04-28 02:39:46 +02:00
parent 2a3fae4d6a
commit aa15807063
8 changed files with 568 additions and 16 deletions

View File

@@ -1095,6 +1095,69 @@ export async function composeSignedDocEmail(
};
}
// ─── Watchers (PR5) ───────────────────────────────────────────────────────────
export async function listDocumentWatchers(documentId: string, portId: string) {
await getDocumentById(documentId, portId); // port-scope check
return db
.select({
userId: documentWatchers.userId,
addedBy: documentWatchers.addedBy,
addedAt: documentWatchers.addedAt,
})
.from(documentWatchers)
.where(eq(documentWatchers.documentId, documentId));
}
export async function addDocumentWatcher(
documentId: string,
portId: string,
userId: string,
meta: AuditMeta,
): Promise<{ userId: string; addedAt: Date }> {
await getDocumentById(documentId, portId);
const [row] = await db
.insert(documentWatchers)
.values({ documentId, userId, addedBy: meta.userId })
.onConflictDoNothing()
.returning();
void createAuditLog({
userId: meta.userId,
portId,
action: 'create',
entityType: 'document_watcher',
entityId: documentId,
newValue: { documentId, watcherUserId: userId },
ipAddress: meta.ipAddress,
userAgent: meta.userAgent,
});
emitToRoom(`port:${portId}`, 'document:updated', { documentId });
return row ? { userId: row.userId, addedAt: row.addedAt } : { userId, addedAt: new Date() };
}
export async function removeDocumentWatcher(
documentId: string,
portId: string,
userId: string,
meta: AuditMeta,
): Promise<void> {
await getDocumentById(documentId, portId);
await db
.delete(documentWatchers)
.where(and(eq(documentWatchers.documentId, documentId), eq(documentWatchers.userId, userId)));
void createAuditLog({
userId: meta.userId,
portId,
action: 'delete',
entityType: 'document_watcher',
entityId: documentId,
oldValue: { documentId, watcherUserId: userId },
ipAddress: meta.ipAddress,
userAgent: meta.userAgent,
});
emitToRoom(`port:${portId}`, 'document:updated', { documentId });
}
/**
* Skeleton for the create-document wizard entry point (PR6).
*