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:
@@ -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).
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user