Files
lagos-back/src/controllers/case.controller.ts
T
mstfyldzandClaude Sonnet 5 50f6cbb1b2 feat: narrative "Dosya Özeti" report instead of field-by-field JSON
summarizeCase now produces a full Markdown narrative report (Olayın
Özeti, Yargılama Süreci ve Kararlar, Taraf İddia/Savunmaları, Önemli
Bulgular ve Belgeler, Sonuç) instead of short structured fields - the
iddianame pre-analysis (when found) still runs first and is fed in as
context. The raw AI text is stored as {ozet: string} rather than
JSON-parsed, since asking the model to escape a multi-paragraph report
inside JSON was fragile.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 20:09:31 +03:00

159 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Response } from 'express';
import { supabase } from '../lib/supabase';
import { AuthenticatedRequest } from '../middleware/auth';
import { runIddianameAnalysis, runCaseNarrativeAnalysis } from './document.controller';
import { findOrCreateCaseByTitle } from '../lib/caseLookup';
export const createCase = async (req: AuthenticatedRequest, res: Response) => {
try {
const userId = req.user?.id;
const title = String(req.body?.title || 'Yeni Sohbet').slice(0, 200);
// kind ayrımı: 'case' = Dava Dosyaları'nda listelenen gerçek dosyalar,
// 'chat' = Sohbet ekranındaki "Yeni Sohbet" ile açılan, dosya olmayan konuşmalar.
// Dava Dosyaları listesi sadece kind='case' olanları gösteriyor.
const kind = req.body?.kind === 'chat' ? 'chat' : 'case';
// requireAuth doğrudan kullanıcının kendi id'sini set ediyor; başkasının hesabına
// dosya oluşturulamaz. Yazma frontend'in anon client'ı yerine burada (service_role
// ile) yapılıyor, cases için de RLS engeli yaşanmasın diye.
const { data, error } = await supabase
.from('cases')
.insert([{ title, user_id: userId, kind }])
.select('id, title')
.single();
if (error) throw error;
res.json({ case: data });
} catch (error: any) {
console.error('Create Case Error:', error);
res.status(500).json({ error: error.message || 'Internal Server Error' });
}
};
// Türkçe İ/I harflerinin regex /i bayrağıyla güvenilir eşleşmemesi (Unicode
// case-folding sorunu) yüzünden manuel normalize ediyoruz.
function normalizeTr(s: string): string {
return s.replace(/İ/g, 'i').replace(/I/g, 'i').toLowerCase();
}
// Dava dosyasındaki TÜM belgelerin çıkarılmış metnini birleştirip kapsamlı, anlatı
// biçiminde bir "Dosya Özeti" raporu üretir (Dosya Ayrıntı ekranındaki "Özet Hazırla"
// butonu). uploadDocument'ın aksine tek bir belgeye değil, dosyanın o anki tüm
// belgelerine bakıyor.
//
// Dosyada bir iddianame varsa önce SADECE onun üzerinden dar kapsamlı bir ön
// çıkarım yapılıyor (isnat edilen suç, iddia özeti, kritik tarihler); anlatı
// raporu bu ön çıkarımı bağlam olarak alıp tüm belgelerle birlikte üretiliyor —
// böylece iddianamedeki temel olgular raporun başlangıç noktası oluyor.
//
// Not: "Bu dosyadaki tüm evraklar işlenene kadar bekle" kontrolü burada DEĞİL,
// frontend'de (CaseDetail) yapılıyor — çünkü eklentiden indirilip henüz OCR/kayıt
// kuyruğunda bekleyen evraklar sadece Electron'un yerel durumunda görünüyor,
// backend'in bundan haberi yok.
export const summarizeCase = async (req: AuthenticatedRequest, res: Response) => {
try {
const { caseId } = req.params;
if (!caseId) {
return res.status(400).json({ error: 'Geçersiz caseId' });
}
const { data: docs, error: docsError } = await supabase
.from('documents')
.select('filename, extracted_text')
.eq('case_id', caseId)
.order('uploaded_at', { ascending: true });
if (docsError) throw docsError;
const withText = (docs || []).filter((d) => d.extracted_text && String(d.extracted_text).trim());
if (withText.length === 0) {
return res.status(400).json({ error: 'Bu dosyada metni çıkarılmış belge yok, özet hazırlanamıyor.' });
}
const iddianameDoc = withText.find((d) =>
normalizeTr(d.filename).includes('iddianame') ||
normalizeTr(String(d.extracted_text)).slice(0, 500).includes('iddianame')
);
let iddianameJson: any = null;
if (iddianameDoc) {
try {
const iddianameResult = await runIddianameAnalysis(String(iddianameDoc.extracted_text));
iddianameJson = iddianameResult.summaryJson;
} catch (e) {
console.error('İddianame ön analizi başarısız, rapora iddianamesiz devam ediliyor:', e);
}
}
// Tüm belgeleri modele gönderiyoruz; ~15000 karaktere kırpılıyor, o yüzden belge
// sayısına göre kabaca dengeli bir pay ayırıyoruz.
const perDocLimit = Math.max(1000, Math.floor(15000 / withText.length));
const combinedText = withText
.map((d) => `--- ${d.filename} ---\n${String(d.extracted_text).slice(0, perDocLimit)}`)
.join('\n\n');
const analysisInput = iddianameJson
? `İDDİANAME ÖN ANALİZİ (rapora bunu öncelikli bağlam olarak yansıt):\n${JSON.stringify(iddianameJson)}\n\n---\n\nDOSYADAKİ TÜM BELGELER:\n${combinedText}`
: combinedText;
const { summaryJson, aiModel } = await runCaseNarrativeAnalysis(analysisInput);
const { data: analysisRecord, error: analysisError } = await supabase
.from('analyses')
.insert([{ case_id: caseId, document_id: null, summary_json: summaryJson, model_used: aiModel }])
.select()
.single();
if (analysisError) throw analysisError;
res.json({ analysis: analysisRecord });
} catch (error: any) {
console.error('Summarize Case Error:', error);
res.status(500).json({ error: error.message || 'Internal Server Error' });
}
};
// UYAP Evrak İndirici eklentisinin "Taraf Bilgileri" sekmesinden yakaladığı
// davacı/davalı/sanık + vekil bilgisini kaydeder. AI'nin OCR metninden tahmin
// ettiği "Taraflar" alanından çok daha güvenilir — doğrudan UYAP'ın yapılandırılmış
// verisi. case_title ile eşleştirme registerLocalDocument ile aynı mantık.
export const updateCaseParties = async (req: AuthenticatedRequest, res: Response) => {
try {
const userId = req.user?.id;
const { case_title, parties } = req.body || {};
if (!case_title || !Array.isArray(parties) || parties.length === 0) {
return res.status(400).json({ error: 'case_title ve parties zorunludur.' });
}
const caseId = await findOrCreateCaseByTitle(userId as string, case_title);
const { data, error } = await supabase
.from('cases')
.update({ parties })
.eq('id', caseId)
.select('id, title, parties')
.single();
if (error) throw error;
res.json({ case: data });
} catch (error: any) {
console.error('Update Case Parties Error:', error);
res.status(500).json({ error: error.message || 'Internal Server Error' });
}
};
export const deleteCase = async (req: AuthenticatedRequest, res: Response) => {
try {
const { caseId } = req.params;
if (!caseId) {
return res.status(400).json({ error: 'Geçersiz caseId' });
}
// requireAuth zaten bu caseId'nin isteği yapan kullanıcıya ait olduğunu doğruladı.
// documents/analyses/chat_messages tabloları case_id üzerinden ON DELETE CASCADE
// ile tanımlı (bkz. sql/1.sql), yani cases satırını silmek hepsini birlikte siliyor.
const { error } = await supabase.from('cases').delete().eq('id', caseId);
if (error) throw error;
res.json({ ok: true });
} catch (error: any) {
console.error('Delete Case Error:', error);
res.status(500).json({ error: error.message || 'Internal Server Error' });
}
};