Compare commits
2 Commits
79191c3810
...
e5d9d3cef3
| Author | SHA1 | Date | |
|---|---|---|---|
| e5d9d3cef3 | |||
|
|
d7bdeb7da2 |
17
drizzle/0004_document_source_scope.sql
Normal file
17
drizzle/0004_document_source_scope.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- Add source scope to documents table (AIIA-66)
|
||||
-- Documents are either case-specific or globally available
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "document_source_scope" AS ENUM ('case', 'global');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE "documents"
|
||||
ADD COLUMN IF NOT EXISTS "source_scope" "document_source_scope" NOT NULL DEFAULT 'case';
|
||||
|
||||
-- Auto-set global scope for norm documents (they are always globally available)
|
||||
UPDATE "documents" SET "source_scope" = 'global' WHERE "category" = 'norm';
|
||||
|
||||
-- Index for efficient source scope queries
|
||||
CREATE INDEX IF NOT EXISTS "documents_source_scope_idx" ON "documents" ("source_scope");
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import SourceSelection from '@/components/documents/source-selection';
|
||||
|
||||
interface CaseOption {
|
||||
id: string;
|
||||
@@ -19,6 +20,7 @@ export default function AnalyseForm({ cases }: { cases: CaseOption[] }) {
|
||||
const [mode, setMode] = useState<string>('gutachten');
|
||||
const [caseId, setCaseId] = useState('');
|
||||
const [question, setQuestion] = useState('');
|
||||
const [selectedDocumentIds, setSelectedDocumentIds] = useState<string[]>([]);
|
||||
const [result, setResult] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
@@ -40,6 +42,7 @@ export default function AnalyseForm({ cases }: { cases: CaseOption[] }) {
|
||||
title: `${MODES.find((m) => m.key === mode)?.label ?? mode} — ${question.trim().slice(0, 80)}`,
|
||||
query: question.trim(),
|
||||
caseId: caseId || undefined,
|
||||
documentIds: selectedDocumentIds.length > 0 ? selectedDocumentIds : undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -110,6 +113,12 @@ export default function AnalyseForm({ cases }: { cases: CaseOption[] }) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SourceSelection
|
||||
caseId={caseId || undefined}
|
||||
selectedIds={selectedDocumentIds}
|
||||
onChange={setSelectedDocumentIds}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<div className="text-sm text-danger bg-danger/10 rounded-lg px-3 py-2">{error}</div>
|
||||
)}
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
|
||||
const { ctx } = auth;
|
||||
|
||||
const body = await request.json();
|
||||
const { mode, title, query, caseId, normIds, decisionIds, stichtag } = body;
|
||||
const { mode, title, query, caseId, normIds, decisionIds, documentIds, stichtag } = body;
|
||||
|
||||
if (!mode || !VALID_MODES.has(mode)) {
|
||||
return Response.json(
|
||||
@@ -43,6 +43,7 @@ export async function POST(request: NextRequest) {
|
||||
query,
|
||||
normIds,
|
||||
decisionIds,
|
||||
documentIds,
|
||||
stichtag,
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { logAuditEvent } from '@/lib/auth/audit';
|
||||
import { requirePermission } from '@/lib/auth/rbac';
|
||||
|
||||
const VALID_CATEGORIES = new Set(['entscheidung', 'norm', 'falldokument', 'sonstiges']);
|
||||
const VALID_SOURCE_SCOPES = new Set(['case', 'global']);
|
||||
|
||||
/** Convert empty/whitespace-only strings to undefined (FormData sends "" for blank fields). */
|
||||
function emptyToUndefined(value: string | null): string | undefined {
|
||||
@@ -22,6 +23,7 @@ export async function POST(request: NextRequest) {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get('file');
|
||||
const category = (formData.get('category') as string) || 'sonstiges';
|
||||
const sourceScope = emptyToUndefined(formData.get('sourceScope') as string | null);
|
||||
const caseId = emptyToUndefined(formData.get('caseId') as string | null);
|
||||
const decisionId = emptyToUndefined(formData.get('decisionId') as string | null);
|
||||
const normInstrumentId = emptyToUndefined(formData.get('normInstrumentId') as string | null);
|
||||
@@ -50,6 +52,9 @@ export async function POST(request: NextRequest) {
|
||||
userId: ctx.userId,
|
||||
file,
|
||||
category: category as 'entscheidung' | 'norm' | 'falldokument' | 'sonstiges',
|
||||
sourceScope: sourceScope && VALID_SOURCE_SCOPES.has(sourceScope)
|
||||
? sourceScope as 'case' | 'global'
|
||||
: undefined,
|
||||
caseId,
|
||||
decisionId,
|
||||
normInstrumentId,
|
||||
@@ -81,6 +86,7 @@ export async function GET(request: NextRequest) {
|
||||
const limit = Math.min(parseInt(searchParams.get('limit') ?? '20', 10), 100);
|
||||
const offset = parseInt(searchParams.get('offset') ?? '0', 10);
|
||||
const category = searchParams.get('category') as string | null;
|
||||
const sourceScope = searchParams.get('sourceScope') as string | null;
|
||||
const caseId = emptyToUndefined(searchParams.get('caseId'));
|
||||
const decisionId = emptyToUndefined(searchParams.get('decisionId'));
|
||||
const normInstrumentId = emptyToUndefined(searchParams.get('normInstrumentId'));
|
||||
@@ -91,6 +97,9 @@ export async function GET(request: NextRequest) {
|
||||
category: category && VALID_CATEGORIES.has(category)
|
||||
? category as 'entscheidung' | 'norm' | 'falldokument' | 'sonstiges'
|
||||
: undefined,
|
||||
sourceScope: sourceScope && VALID_SOURCE_SCOPES.has(sourceScope)
|
||||
? sourceScope as 'case' | 'global'
|
||||
: undefined,
|
||||
caseId,
|
||||
decisionId,
|
||||
normInstrumentId,
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useState, useRef, useCallback, useEffect } from 'react';
|
||||
|
||||
interface DokumentUploadProps {
|
||||
category: 'entscheidung' | 'norm' | 'falldokument' | 'sonstiges';
|
||||
/** Source scope — case-specific or globally available */
|
||||
sourceScope?: 'case' | 'global';
|
||||
/** Optional linked entity ID */
|
||||
caseId?: string;
|
||||
decisionId?: string;
|
||||
@@ -115,6 +117,7 @@ function IngestionProgress({ doc, debug }: { doc: DocumentItem; debug: boolean }
|
||||
|
||||
export default function DokumentUpload({
|
||||
category,
|
||||
sourceScope,
|
||||
caseId,
|
||||
decisionId,
|
||||
normInstrumentId,
|
||||
@@ -181,6 +184,7 @@ export default function DokumentUpload({
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('category', category);
|
||||
if (sourceScope) formData.append('sourceScope', sourceScope);
|
||||
if (caseId) formData.append('caseId', caseId);
|
||||
if (decisionId) formData.append('decisionId', decisionId);
|
||||
if (normInstrumentId) formData.append('normInstrumentId', normInstrumentId);
|
||||
|
||||
206
src/components/documents/source-selection.tsx
Normal file
206
src/components/documents/source-selection.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
interface DocumentSource {
|
||||
id: string;
|
||||
filename: string;
|
||||
category: string;
|
||||
sourceScope: string;
|
||||
status: string;
|
||||
caseId: string | null;
|
||||
}
|
||||
|
||||
interface SourceSelectionProps {
|
||||
caseId?: string;
|
||||
selectedIds: string[];
|
||||
onChange: (ids: string[]) => void;
|
||||
}
|
||||
|
||||
const CATEGORY_LABELS: Record<string, string> = {
|
||||
entscheidung: 'Entscheidung',
|
||||
norm: 'Norm',
|
||||
falldokument: 'Falldokument',
|
||||
sonstiges: 'Sonstiges',
|
||||
};
|
||||
|
||||
const SCOPE_LABELS: Record<string, string> = {
|
||||
global: 'Global',
|
||||
case: 'Fallbezogen',
|
||||
};
|
||||
|
||||
export default function SourceSelection({ caseId, selectedIds, onChange }: SourceSelectionProps) {
|
||||
const [sources, setSources] = useState<DocumentSource[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const fetchSources = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Fetch global documents + case-specific documents (if a case is selected)
|
||||
const params = new URLSearchParams();
|
||||
const results: DocumentSource[] = [];
|
||||
|
||||
// Always fetch global documents
|
||||
const globalRes = await fetch('/api/documents?sourceScope=global');
|
||||
if (globalRes.ok) {
|
||||
const data = await globalRes.json();
|
||||
results.push(...data.filter((d: DocumentSource) => d.status === 'extracted'));
|
||||
}
|
||||
|
||||
// Fetch case documents if case is selected
|
||||
if (caseId) {
|
||||
const caseRes = await fetch(`/api/documents?caseId=${caseId}&sourceScope=case`);
|
||||
if (caseRes.ok) {
|
||||
const data = await caseRes.json();
|
||||
results.push(...data.filter((d: DocumentSource) => d.status === 'extracted'));
|
||||
}
|
||||
}
|
||||
|
||||
setSources(results);
|
||||
} catch {
|
||||
// Silently fail
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [caseId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSources();
|
||||
}, [fetchSources]);
|
||||
|
||||
function toggleSource(id: string) {
|
||||
if (selectedIds.includes(id)) {
|
||||
onChange(selectedIds.filter((s) => s !== id));
|
||||
} else {
|
||||
onChange([...selectedIds, id]);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAll() {
|
||||
if (selectedIds.length === sources.length) {
|
||||
onChange([]);
|
||||
} else {
|
||||
onChange(sources.map((s) => s.id));
|
||||
}
|
||||
}
|
||||
|
||||
// Group by scope
|
||||
const globalSources = sources.filter((s) => s.sourceScope === 'global');
|
||||
const caseSources = sources.filter((s) => s.sourceScope === 'case');
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-xs text-muted py-2">Quellen werden geladen...</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sources.length === 0) {
|
||||
return (
|
||||
<div className="text-xs text-muted py-2">
|
||||
Keine Dokumente verfügbar. Laden Sie Dokumente hoch, um sie als KI-Quellen zu verwenden.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border border-card-border rounded-lg">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="w-full flex items-center justify-between px-3 py-2.5 text-sm text-foreground hover:bg-gray-50 transition-colors rounded-lg"
|
||||
>
|
||||
<span className="font-medium">
|
||||
KI-Quellen ({selectedIds.length}/{sources.length} ausgewählt)
|
||||
</span>
|
||||
<svg
|
||||
className={`w-4 h-4 text-muted transition-transform ${expanded ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="border-t border-card-border px-3 py-2 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-muted">
|
||||
Wählen Sie die Dokumente, die die KI berücksichtigen soll.
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleAll}
|
||||
className="text-xs text-primary hover:underline"
|
||||
>
|
||||
{selectedIds.length === sources.length ? 'Alle abwählen' : 'Alle auswählen'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{globalSources.length > 0 && (
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-muted uppercase tracking-wide mb-1">
|
||||
Globale Quellen (Normen / Gesetze)
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
{globalSources.map((src) => (
|
||||
<SourceItem
|
||||
key={src.id}
|
||||
source={src}
|
||||
selected={selectedIds.includes(src.id)}
|
||||
onToggle={() => toggleSource(src.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{caseSources.length > 0 && (
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-muted uppercase tracking-wide mb-1">
|
||||
Fallbezogene Dokumente
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
{caseSources.map((src) => (
|
||||
<SourceItem
|
||||
key={src.id}
|
||||
source={src}
|
||||
selected={selectedIds.includes(src.id)}
|
||||
onToggle={() => toggleSource(src.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SourceItem({
|
||||
source,
|
||||
selected,
|
||||
onToggle,
|
||||
}: {
|
||||
source: DocumentSource;
|
||||
selected: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
return (
|
||||
<label className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-50 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected}
|
||||
onChange={onToggle}
|
||||
className="rounded border-gray-300 text-primary focus:ring-primary/30"
|
||||
/>
|
||||
<span className="text-sm text-foreground truncate flex-1">{source.filename}</span>
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-gray-100 text-muted shrink-0">
|
||||
{CATEGORY_LABELS[source.category] ?? source.category}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import { getModelForTenant } from './providers';
|
||||
import { SYSTEM_PROMPTS, buildContextBlock, type AnalysisModeKey } from './prompts';
|
||||
import { ANALYSIS_MODES } from './modes';
|
||||
import { AnalyseMode } from '@/types';
|
||||
import { db } from '@/lib/db';
|
||||
import { norms, normInstruments, decisions, analyses } from '@/lib/db/schema';
|
||||
import { db, withTenantDb } from '@/lib/db';
|
||||
import { norms, normInstruments, decisions, analyses, documents } from '@/lib/db/schema';
|
||||
import { eq, and, lte, or, isNull, gte, inArray } from 'drizzle-orm';
|
||||
|
||||
interface AnalysisInput {
|
||||
@@ -20,6 +20,8 @@ interface AnalysisInput {
|
||||
normIds?: string[];
|
||||
/** Optional: specific decision IDs to include as context */
|
||||
decisionIds?: string[];
|
||||
/** Optional: specific document IDs to include as context */
|
||||
documentIds?: string[];
|
||||
/** Optional: reference date for norm versioning (Stichtag) */
|
||||
stichtag?: string;
|
||||
}
|
||||
@@ -89,6 +91,39 @@ async function fetchDecisionContext(
|
||||
.limit(decisionIds?.length ? 50 : 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch document content for the analysis context.
|
||||
* When documentIds are given, fetch those specific documents.
|
||||
* Respects source scope: global documents are always available,
|
||||
* case documents only within their case context.
|
||||
*/
|
||||
async function fetchDocumentContext(
|
||||
tenantId: string,
|
||||
documentIds?: string[],
|
||||
caseId?: string,
|
||||
) {
|
||||
if (!documentIds?.length) return [];
|
||||
|
||||
return withTenantDb(tenantId, async (tdb) => {
|
||||
const conditions = [
|
||||
inArray(documents.id, documentIds),
|
||||
eq(documents.status, 'extracted'),
|
||||
];
|
||||
|
||||
return tdb
|
||||
.select({
|
||||
id: documents.id,
|
||||
filename: documents.filename,
|
||||
category: documents.category,
|
||||
sourceScope: documents.sourceScope,
|
||||
extractedText: documents.extractedText,
|
||||
})
|
||||
.from(documents)
|
||||
.where(and(...conditions))
|
||||
.limit(20);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an analysis record in the database and return a streaming response.
|
||||
*/
|
||||
@@ -97,16 +132,19 @@ export async function runAnalysis(input: AnalysisInput) {
|
||||
const systemPromptKey = modeConfig.systemPromptKey as AnalysisModeKey;
|
||||
|
||||
// Fetch context in parallel
|
||||
const [normContext, decisionContext] = await Promise.all([
|
||||
const [normContext, decisionContext, documentContext] = await Promise.all([
|
||||
modeConfig.requiresNorms
|
||||
? fetchNormContext(input.tenantId, input.normIds, input.stichtag)
|
||||
: Promise.resolve([]),
|
||||
modeConfig.requiresDecisions
|
||||
? fetchDecisionContext(input.tenantId, input.decisionIds)
|
||||
: Promise.resolve([]),
|
||||
input.documentIds?.length
|
||||
? fetchDocumentContext(input.tenantId, input.documentIds, input.caseId)
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
const contextBlock = buildContextBlock(normContext, decisionContext);
|
||||
const contextBlock = buildContextBlock(normContext, decisionContext, documentContext);
|
||||
|
||||
const { model, provider, modelId } = await getModelForTenant(input.tenantId);
|
||||
|
||||
@@ -126,7 +164,7 @@ export async function runAnalysis(input: AnalysisInput) {
|
||||
sources: {
|
||||
normIds: normContext.map((n) => n.id),
|
||||
decisionIds: decisionContext.map((d) => d.id),
|
||||
otherSources: [],
|
||||
otherSources: documentContext.map((d) => d.id),
|
||||
},
|
||||
})
|
||||
.returning();
|
||||
@@ -168,16 +206,19 @@ export async function runAnalysisSync(input: AnalysisInput) {
|
||||
const modeConfig = ANALYSIS_MODES[input.mode];
|
||||
const systemPromptKey = modeConfig.systemPromptKey as AnalysisModeKey;
|
||||
|
||||
const [normContext, decisionContext] = await Promise.all([
|
||||
const [normContext, decisionContext, documentContext] = await Promise.all([
|
||||
modeConfig.requiresNorms
|
||||
? fetchNormContext(input.tenantId, input.normIds, input.stichtag)
|
||||
: Promise.resolve([]),
|
||||
modeConfig.requiresDecisions
|
||||
? fetchDecisionContext(input.tenantId, input.decisionIds)
|
||||
: Promise.resolve([]),
|
||||
input.documentIds?.length
|
||||
? fetchDocumentContext(input.tenantId, input.documentIds, input.caseId)
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
const contextBlock = buildContextBlock(normContext, decisionContext);
|
||||
const contextBlock = buildContextBlock(normContext, decisionContext, documentContext);
|
||||
const userMessage = contextBlock
|
||||
? `${contextBlock}\n\n---\n\n## Rechtsfrage\n\n${input.query}`
|
||||
: input.query;
|
||||
@@ -199,7 +240,7 @@ export async function runAnalysisSync(input: AnalysisInput) {
|
||||
sources: {
|
||||
normIds: normContext.map((n) => n.id),
|
||||
decisionIds: decisionContext.map((d) => d.id),
|
||||
otherSources: [],
|
||||
otherSources: documentContext.map((d) => d.id),
|
||||
},
|
||||
})
|
||||
.returning();
|
||||
@@ -230,6 +271,7 @@ export async function runAnalysisSync(input: AnalysisInput) {
|
||||
sources: {
|
||||
normIds: normContext.map((n) => n.id),
|
||||
decisionIds: decisionContext.map((d) => d.id),
|
||||
documentIds: documentContext.map((d) => d.id),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,6 +118,12 @@ export function buildContextBlock(
|
||||
headnote: string | null;
|
||||
reasoning: string | null;
|
||||
}>,
|
||||
documents?: Array<{
|
||||
filename: string;
|
||||
category: string;
|
||||
sourceScope: string;
|
||||
extractedText: string | null;
|
||||
}>,
|
||||
): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
@@ -149,5 +155,17 @@ export function buildContextBlock(
|
||||
}
|
||||
}
|
||||
|
||||
const docsWithText = documents?.filter((d) => d.extractedText) ?? [];
|
||||
if (docsWithText.length > 0) {
|
||||
parts.push('## Hochgeladene Dokumente\n');
|
||||
for (const d of docsWithText) {
|
||||
const scopeLabel = d.sourceScope === 'global' ? 'Global' : 'Fallbezogen';
|
||||
parts.push(`### ${d.filename} [${scopeLabel}]`);
|
||||
const text = d.extractedText!;
|
||||
parts.push(text.slice(0, 2000) + (text.length > 2000 ? '…' : ''));
|
||||
parts.push('');
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
@@ -896,6 +896,12 @@ export const nonRenewalDeadlines = pgTable(
|
||||
// Dokumente (Generic Document Upload — Phase 3.4)
|
||||
// ============================================================
|
||||
|
||||
/** Document source scope — case-specific vs globally available */
|
||||
export const documentSourceScopeEnum = pgEnum("document_source_scope", [
|
||||
"case", // Case-specific document (only available within its case)
|
||||
"global", // Globally available (laws, regulations — available everywhere)
|
||||
]);
|
||||
|
||||
/** Document category — what kind of document this is */
|
||||
export const documentCategoryEnum = pgEnum("document_category", [
|
||||
"entscheidung", // Court decision / arbitration award document
|
||||
@@ -924,6 +930,8 @@ export const documents = pgTable(
|
||||
userId: uuid("user_id").notNull().references(() => users.id),
|
||||
/** Document category */
|
||||
category: documentCategoryEnum("category").notNull().default("sonstiges"),
|
||||
/** Source scope: case-specific or globally available */
|
||||
sourceScope: documentSourceScopeEnum("source_scope").notNull().default("case"),
|
||||
/** Optional link to a case */
|
||||
caseId: uuid("case_id").references(() => cases.id, { onDelete: "set null" }),
|
||||
/** Optional link to a decision */
|
||||
|
||||
@@ -14,12 +14,14 @@ const ALLOWED_MIME_TYPES = new Set([
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
|
||||
|
||||
type DocumentCategory = 'entscheidung' | 'norm' | 'falldokument' | 'sonstiges';
|
||||
type DocumentSourceScope = 'case' | 'global';
|
||||
|
||||
interface UploadOptions {
|
||||
tenantId: string;
|
||||
userId: string;
|
||||
file: File;
|
||||
category: DocumentCategory;
|
||||
sourceScope?: DocumentSourceScope;
|
||||
caseId?: string;
|
||||
decisionId?: string;
|
||||
normInstrumentId?: string;
|
||||
@@ -36,7 +38,9 @@ interface UploadResult {
|
||||
* Text extraction happens asynchronously via extractDocumentText().
|
||||
*/
|
||||
export async function uploadDocument(opts: UploadOptions): Promise<UploadResult> {
|
||||
const { tenantId, userId, file, category, caseId, decisionId, normInstrumentId } = opts;
|
||||
const { tenantId, userId, file, category, sourceScope, caseId, decisionId, normInstrumentId } = opts;
|
||||
// Norm documents are always global; case documents default to case-scoped
|
||||
const resolvedScope = sourceScope ?? (category === 'norm' ? 'global' : 'case');
|
||||
|
||||
if (!ALLOWED_MIME_TYPES.has(file.type)) {
|
||||
throw new Error(
|
||||
@@ -70,6 +74,7 @@ export async function uploadDocument(opts: UploadOptions): Promise<UploadResult>
|
||||
tenantId,
|
||||
userId,
|
||||
category,
|
||||
sourceScope: resolvedScope,
|
||||
caseId: caseId ?? null,
|
||||
decisionId: decisionId ?? null,
|
||||
normInstrumentId: normInstrumentId ?? null,
|
||||
@@ -196,6 +201,7 @@ export async function listDocuments(
|
||||
tenantId: string,
|
||||
filters?: {
|
||||
category?: DocumentCategory;
|
||||
sourceScope?: DocumentSourceScope;
|
||||
caseId?: string;
|
||||
decisionId?: string;
|
||||
normInstrumentId?: string;
|
||||
@@ -209,6 +215,9 @@ export async function listDocuments(
|
||||
if (filters?.category) {
|
||||
conditions.push(eq(documents.category, filters.category));
|
||||
}
|
||||
if (filters?.sourceScope) {
|
||||
conditions.push(eq(documents.sourceScope, filters.sourceScope));
|
||||
}
|
||||
if (filters?.caseId) {
|
||||
conditions.push(eq(documents.caseId, filters.caseId));
|
||||
}
|
||||
@@ -226,6 +235,7 @@ export async function listDocuments(
|
||||
mimeType: documents.mimeType,
|
||||
fileSizeBytes: documents.fileSizeBytes,
|
||||
category: documents.category,
|
||||
sourceScope: documents.sourceScope,
|
||||
status: documents.status,
|
||||
errorMessage: documents.errorMessage,
|
||||
caseId: documents.caseId,
|
||||
|
||||
Reference in New Issue
Block a user