- Add Cloud for Sovereignty landing zone architecture and deployment - Implement complete legal document management system - Reorganize documentation with improved navigation - Add infrastructure improvements (Dockerfiles, K8s, monitoring) - Add operational improvements (graceful shutdown, rate limiting, caching) - Create comprehensive project structure documentation - Add Azure deployment automation scripts - Improve repository navigation and organization
266 lines
6.7 KiB
TypeScript
266 lines
6.7 KiB
TypeScript
/**
|
|
* Document Comments and Annotations
|
|
* Handles comments, annotations, and collaborative review features
|
|
*/
|
|
|
|
import { query } from './client';
|
|
import { z } from 'zod';
|
|
|
|
export const DocumentCommentSchema = z.object({
|
|
id: z.string().uuid(),
|
|
document_id: z.string().uuid(),
|
|
version_id: z.string().uuid().optional(),
|
|
parent_comment_id: z.string().uuid().optional(),
|
|
comment_text: z.string(),
|
|
comment_type: z.enum(['comment', 'suggestion', 'question', 'resolution']),
|
|
status: z.enum(['open', 'resolved', 'dismissed']),
|
|
page_number: z.number().int().optional(),
|
|
x_position: z.number().optional(),
|
|
y_position: z.number().optional(),
|
|
highlight_text: z.string().optional(),
|
|
author_id: z.string().uuid(),
|
|
resolved_by: z.string().uuid().optional(),
|
|
resolved_at: z.date().optional(),
|
|
created_at: z.date(),
|
|
updated_at: z.date(),
|
|
});
|
|
|
|
export type DocumentComment = z.infer<typeof DocumentCommentSchema>;
|
|
|
|
export interface CreateDocumentCommentInput {
|
|
document_id: string;
|
|
version_id?: string;
|
|
parent_comment_id?: string;
|
|
comment_text: string;
|
|
comment_type?: 'comment' | 'suggestion' | 'question' | 'resolution';
|
|
status?: 'open' | 'resolved' | 'dismissed';
|
|
page_number?: number;
|
|
x_position?: number;
|
|
y_position?: number;
|
|
highlight_text?: string;
|
|
author_id: string;
|
|
}
|
|
|
|
/**
|
|
* Create a document comment
|
|
*/
|
|
export async function createDocumentComment(
|
|
input: CreateDocumentCommentInput
|
|
): Promise<DocumentComment> {
|
|
const result = await query<DocumentComment>(
|
|
`INSERT INTO document_comments
|
|
(document_id, version_id, parent_comment_id, comment_text, comment_type,
|
|
status, page_number, x_position, y_position, highlight_text, author_id)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
RETURNING *`,
|
|
[
|
|
input.document_id,
|
|
input.version_id || null,
|
|
input.parent_comment_id || null,
|
|
input.comment_text,
|
|
input.comment_type || 'comment',
|
|
input.status || 'open',
|
|
input.page_number || null,
|
|
input.x_position || null,
|
|
input.y_position || null,
|
|
input.highlight_text || null,
|
|
input.author_id,
|
|
]
|
|
);
|
|
|
|
return result.rows[0]!;
|
|
}
|
|
|
|
/**
|
|
* Get comment by ID
|
|
*/
|
|
export async function getDocumentComment(id: string): Promise<DocumentComment | null> {
|
|
const result = await query<DocumentComment>(
|
|
`SELECT * FROM document_comments WHERE id = $1`,
|
|
[id]
|
|
);
|
|
return result.rows[0] || null;
|
|
}
|
|
|
|
/**
|
|
* Get all comments for a document
|
|
*/
|
|
export async function getDocumentComments(
|
|
document_id: string,
|
|
version_id?: string,
|
|
include_resolved = false
|
|
): Promise<DocumentComment[]> {
|
|
const conditions = ['document_id = $1'];
|
|
const params: unknown[] = [document_id];
|
|
let paramIndex = 2;
|
|
|
|
if (version_id) {
|
|
conditions.push(`version_id = $${paramIndex++}`);
|
|
params.push(version_id);
|
|
}
|
|
|
|
if (!include_resolved) {
|
|
conditions.push(`status != 'resolved'`);
|
|
}
|
|
|
|
const result = await query<DocumentComment>(
|
|
`SELECT * FROM document_comments
|
|
WHERE ${conditions.join(' AND ')}
|
|
ORDER BY created_at ASC`,
|
|
params
|
|
);
|
|
|
|
return result.rows;
|
|
}
|
|
|
|
/**
|
|
* Get threaded comments (with replies)
|
|
*/
|
|
export interface ThreadedComment extends DocumentComment {
|
|
replies?: ThreadedComment[];
|
|
}
|
|
|
|
export async function getThreadedDocumentComments(
|
|
document_id: string,
|
|
version_id?: string
|
|
): Promise<ThreadedComment[]> {
|
|
const allComments = await getDocumentComments(document_id, version_id, true);
|
|
|
|
// Build tree structure
|
|
const commentMap = new Map<string, ThreadedComment>();
|
|
const rootComments: ThreadedComment[] = [];
|
|
|
|
// First pass: create map
|
|
for (const comment of allComments) {
|
|
commentMap.set(comment.id, { ...comment, replies: [] });
|
|
}
|
|
|
|
// Second pass: build tree
|
|
for (const comment of allComments) {
|
|
const threaded = commentMap.get(comment.id)!;
|
|
if (comment.parent_comment_id) {
|
|
const parent = commentMap.get(comment.parent_comment_id);
|
|
if (parent) {
|
|
parent.replies!.push(threaded);
|
|
}
|
|
} else {
|
|
rootComments.push(threaded);
|
|
}
|
|
}
|
|
|
|
return rootComments;
|
|
}
|
|
|
|
/**
|
|
* Update comment
|
|
*/
|
|
export async function updateDocumentComment(
|
|
id: string,
|
|
updates: Partial<Pick<DocumentComment, 'comment_text' | 'status'>>
|
|
): Promise<DocumentComment | null> {
|
|
const fields: string[] = [];
|
|
const values: unknown[] = [];
|
|
let paramIndex = 1;
|
|
|
|
if (updates.comment_text !== undefined) {
|
|
fields.push(`comment_text = $${paramIndex++}`);
|
|
values.push(updates.comment_text);
|
|
}
|
|
|
|
if (updates.status !== undefined) {
|
|
fields.push(`status = $${paramIndex++}`);
|
|
values.push(updates.status);
|
|
|
|
if (updates.status === 'resolved') {
|
|
// Get current user from context - for now, we'll need to pass it
|
|
// This should be handled by the service layer
|
|
}
|
|
}
|
|
|
|
if (fields.length === 0) {
|
|
return getDocumentComment(id);
|
|
}
|
|
|
|
fields.push(`updated_at = NOW()`);
|
|
values.push(id);
|
|
|
|
const result = await query<DocumentComment>(
|
|
`UPDATE document_comments
|
|
SET ${fields.join(', ')}
|
|
WHERE id = $${paramIndex}
|
|
RETURNING *`,
|
|
values
|
|
);
|
|
|
|
return result.rows[0] || null;
|
|
}
|
|
|
|
/**
|
|
* Resolve comment
|
|
*/
|
|
export async function resolveDocumentComment(
|
|
id: string,
|
|
resolved_by: string
|
|
): Promise<DocumentComment | null> {
|
|
const result = await query<DocumentComment>(
|
|
`UPDATE document_comments
|
|
SET status = 'resolved', resolved_by = $1, resolved_at = NOW(), updated_at = NOW()
|
|
WHERE id = $2
|
|
RETURNING *`,
|
|
[resolved_by, id]
|
|
);
|
|
|
|
return result.rows[0] || null;
|
|
}
|
|
|
|
/**
|
|
* Get comment statistics for a document
|
|
*/
|
|
export interface CommentStatistics {
|
|
total: number;
|
|
open: number;
|
|
resolved: number;
|
|
dismissed: number;
|
|
by_type: Record<string, number>;
|
|
}
|
|
|
|
export async function getDocumentCommentStatistics(
|
|
document_id: string
|
|
): Promise<CommentStatistics> {
|
|
const result = await query<{
|
|
total: string;
|
|
open: string;
|
|
resolved: string;
|
|
dismissed: string;
|
|
comment_type: string;
|
|
count: string;
|
|
}>(
|
|
`SELECT
|
|
COUNT(*) as total,
|
|
COUNT(*) FILTER (WHERE status = 'open') as open,
|
|
COUNT(*) FILTER (WHERE status = 'resolved') as resolved,
|
|
COUNT(*) FILTER (WHERE status = 'dismissed') as dismissed,
|
|
comment_type,
|
|
COUNT(*) as count
|
|
FROM document_comments
|
|
WHERE document_id = $1
|
|
GROUP BY comment_type`,
|
|
[document_id]
|
|
);
|
|
|
|
const stats: CommentStatistics = {
|
|
total: parseInt(result.rows[0]?.total || '0', 10),
|
|
open: parseInt(result.rows[0]?.open || '0', 10),
|
|
resolved: parseInt(result.rows[0]?.resolved || '0', 10),
|
|
dismissed: parseInt(result.rows[0]?.dismissed || '0', 10),
|
|
by_type: {},
|
|
};
|
|
|
|
for (const row of result.rows) {
|
|
stats.by_type[row.comment_type] = parseInt(row.count, 10);
|
|
}
|
|
|
|
return stats;
|
|
}
|
|
|