210 lines
8.9 KiB
TypeScript
210 lines
8.9 KiB
TypeScript
import { Response } from 'express';
|
||
import { supabase } from '../lib/supabase';
|
||
import { AuthenticatedRequest } from '../middleware/auth';
|
||
import { runIddianameAnalysis, runCaseNarrativeAnalysis, runStatementAnalysis } 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' });
|
||
}
|
||
};
|
||
|
||
// Çelişki Avcısı: İfade ve sorgu evraklarını alıp analiz eder.
|
||
export const analyzeStatements = 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;
|
||
|
||
// Sadece ifade, sorgu, beyan, tutanak, duruşma içeren belgeleri filtrele
|
||
const keywords = ['ifade', 'sorgu', 'beyan', 'tutanak', 'duruşma'];
|
||
const statementDocs = (docs || []).filter((d) => {
|
||
if (!d.extracted_text || !String(d.extracted_text).trim()) return false;
|
||
const fn = normalizeTr(d.filename);
|
||
return keywords.some(kw => fn.includes(kw));
|
||
});
|
||
|
||
if (statementDocs.length === 0) {
|
||
return res.status(400).json({ error: 'Bu dosyada ifade/beyan evrakı bulunamadı.' });
|
||
}
|
||
|
||
const perDocLimit = Math.max(1000, Math.floor(15000 / statementDocs.length));
|
||
const combinedText = statementDocs
|
||
.map((d) => `--- ${d.filename} ---\n${String(d.extracted_text).slice(0, perDocLimit)}`)
|
||
.join('\n\n');
|
||
|
||
const analysisInput = `Aşağıdaki belgeler sadece ifade ve sorgu evraklarıdır:\n\n${combinedText}`;
|
||
|
||
const { summaryJson, aiModel } = await runStatementAnalysis(analysisInput);
|
||
|
||
// Çelişki analizi sonucunu 'analyses' tablosuna kaydet
|
||
const { data: analysisRecord, error: analysisError } = await supabase
|
||
.from('analyses')
|
||
.insert([{ case_id: caseId, document_id: null, summary_json: { statementAnalysis: summaryJson }, model_used: aiModel }])
|
||
.select()
|
||
.single();
|
||
if (analysisError) throw analysisError;
|
||
|
||
res.json({ analysis: analysisRecord });
|
||
} catch (error: any) {
|
||
console.error('Analyze Statements 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' });
|
||
}
|
||
};
|