449 lines
19 KiB
TypeScript
449 lines
19 KiB
TypeScript
import { Response } from 'express';
|
||
import { supabase } from '../lib/supabase';
|
||
import { AuthenticatedRequest } from '../middleware/auth';
|
||
import { runIddianameAnalysis, runCaseNarrativeAnalysis, runStatementAnalysis, runDeficiencyAnalysis, runStrategyAnalysis, runMediationAnalysis, runDigitalInternQa } 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.
|
||
// "tutanak" tek başına çok geniş — "Tebligat Tutanağı" gibi ifade/beyan
|
||
// İÇERMEYEN evrakları da yakalıyordu, çelişki analizine gürültü katıyordu.
|
||
const keywords = ['ifade', 'sorgu', 'beyan', 'tutanak', 'duruşma'];
|
||
const excludeKeywords = ['tebligat', 'tebliğ'];
|
||
const statementDocs = (docs || []).filter((d) => {
|
||
if (!d.extracted_text || !String(d.extracted_text).trim()) return false;
|
||
const fn = normalizeTr(d.filename);
|
||
if (excludeKeywords.some(kw => fn.includes(kw))) return false;
|
||
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' });
|
||
}
|
||
};
|
||
|
||
export const analyzeDeficiencies = 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 tensip, zaptı, tutanak, karar, müzekkere gibi evrakları filtrele
|
||
const keywords = ['tensip', 'zaptı', 'tutanak', 'duruşma', 'müzekkere', 'karar', 'iddianame'];
|
||
const deficiencyDocs = (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));
|
||
});
|
||
|
||
// Eğer hiç spesifik evrak yoksa, çöp olmayanları al
|
||
let targetDocs = deficiencyDocs;
|
||
if (targetDocs.length === 0) {
|
||
const trashKeywords = ['tebligat', 'makbuz', 'pul', 'zarf', 'masraf', 'havale', 'barkod', 'alındı', 'görüldü', 'ilan', 'talimat', 'nüfus', 'sicil', 'sabıka'];
|
||
targetDocs = (docs || []).filter(d => {
|
||
if (!d.extracted_text || !String(d.extracted_text).trim()) return false;
|
||
const fn = normalizeTr(d.filename);
|
||
return !trashKeywords.some(kw => fn.includes(kw));
|
||
});
|
||
}
|
||
|
||
if (targetDocs.length === 0) {
|
||
return res.status(400).json({ error: 'Bu dosyada analiz edilecek uygun evrak bulunamadı.' });
|
||
}
|
||
|
||
const perDocLimit = Math.max(1000, Math.floor(15000 / targetDocs.length));
|
||
const combinedText = targetDocs
|
||
.map((d) => `--- ${d.filename} ---\n${String(d.extracted_text).slice(0, perDocLimit)}`)
|
||
.join('\n\n');
|
||
|
||
const analysisInput = `Aşağıdaki belgeler dava dosyasına aittir:\n\n${combinedText}`;
|
||
|
||
const { summaryJson, aiModel } = await runDeficiencyAnalysis(analysisInput);
|
||
|
||
// Eksiklik analizi sonucunu 'analyses' tablosuna kaydet
|
||
const { data: analysisRecord, error: analysisError } = await supabase
|
||
.from('analyses')
|
||
.insert([{ case_id: caseId, document_id: null, summary_json: { deficiencyAnalysis: summaryJson }, model_used: aiModel }])
|
||
.select()
|
||
.single();
|
||
if (analysisError) throw analysisError;
|
||
|
||
res.json({ analysis: analysisRecord });
|
||
} catch (error: any) {
|
||
console.error('Analyze Deficiencies Error:', error);
|
||
res.status(500).json({ error: error.message || 'Internal Server Error' });
|
||
}
|
||
};
|
||
|
||
export const analyzeStrategy = async (req: AuthenticatedRequest, res: Response) => {
|
||
try {
|
||
const { caseId } = req.params;
|
||
const { clientName, client_name, clientRole, client_role } = req.body || {};
|
||
const nameToUse = clientName || client_name;
|
||
const roleToUse = clientRole || client_role;
|
||
|
||
if (!caseId) {
|
||
return res.status(400).json({ error: 'Geçersiz caseId' });
|
||
}
|
||
|
||
if (!nameToUse) {
|
||
return res.status(400).json({ error: 'clientName (Müvekkil Adı) zorunludur' });
|
||
}
|
||
|
||
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;
|
||
|
||
// Gereksiz makbuz, pul vb. hariç tüm evrakları strateji için kullan
|
||
const trashKeywords = ['tebligat', 'makbuz', 'pul', 'zarf', 'masraf', 'havale', 'barkod', 'alındı', 'görüldü', 'ilan', 'talimat', 'nüfus', 'sicil', 'sabıka'];
|
||
const targetDocs = (docs || []).filter(d => {
|
||
if (!d.extracted_text || !String(d.extracted_text).trim()) return false;
|
||
const fn = normalizeTr(d.filename);
|
||
return !trashKeywords.some(kw => fn.includes(kw));
|
||
});
|
||
|
||
if (targetDocs.length === 0) {
|
||
return res.status(400).json({ error: 'Bu dosyada strateji analizi yapılabilecek evrak bulunamadı.' });
|
||
}
|
||
|
||
const perDocLimit = Math.max(1000, Math.floor(25000 / targetDocs.length)); // Strateji için context biraz daha geniş (25k)
|
||
const combinedText = targetDocs
|
||
.map((d) => `--- ${d.filename} ---\n${String(d.extracted_text).slice(0, perDocLimit)}`)
|
||
.join('\n\n');
|
||
|
||
const analysisInput = `Aşağıdaki belgeler dava dosyasına aittir:\n\n${combinedText}`;
|
||
|
||
const { summaryJson, aiModel } = await runStrategyAnalysis(analysisInput, nameToUse, roleToUse);
|
||
|
||
// Strateji analizi sonucunu 'analyses' tablosuna kaydet
|
||
const { data: analysisRecord, error: analysisError } = await supabase
|
||
.from('analyses')
|
||
.insert([{ case_id: caseId, document_id: null, summary_json: { strategyAnalysis: summaryJson, clientName: nameToUse, clientRole: roleToUse }, model_used: aiModel }])
|
||
.select()
|
||
.single();
|
||
if (analysisError) throw analysisError;
|
||
|
||
res.json({ analysis: analysisRecord });
|
||
} catch (error: any) {
|
||
console.error('Analyze Strategy Error:', error);
|
||
res.status(500).json({ error: error.message || 'Internal Server Error' });
|
||
}
|
||
};
|
||
|
||
export const analyzeMediation = async (req: AuthenticatedRequest, res: Response) => {
|
||
try {
|
||
const { caseId } = req.params;
|
||
const { clientName, client_name } = req.body || {};
|
||
const nameToUse = clientName || client_name;
|
||
|
||
if (!caseId) {
|
||
return res.status(400).json({ error: 'Geçersiz caseId' });
|
||
}
|
||
|
||
if (!nameToUse) {
|
||
return res.status(400).json({ error: 'clientName (Müvekkil Adı) zorunludur' });
|
||
}
|
||
|
||
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 trashKeywords = ['tebligat', 'makbuz', 'pul', 'zarf', 'masraf', 'havale', 'barkod', 'alındı', 'görüldü', 'ilan', 'talimat', 'nüfus', 'sicil', 'sabıka'];
|
||
const targetDocs = (docs || []).filter(d => {
|
||
if (!d.extracted_text || !String(d.extracted_text).trim()) return false;
|
||
const fn = (d.filename || '').toLowerCase().replace(/ı/g, 'i').replace(/ğ/g, 'g').replace(/ü/g, 'u').replace(/ş/g, 's').replace(/ö/g, 'o').replace(/ç/g, 'c');
|
||
return !trashKeywords.some(kw => fn.includes(kw));
|
||
});
|
||
|
||
if (targetDocs.length === 0) {
|
||
return res.status(400).json({ error: 'Bu dosyada arabuluculuk analizi yapılabilecek evrak bulunamadı.' });
|
||
}
|
||
|
||
const perDocLimit = Math.max(1000, Math.floor(25000 / targetDocs.length));
|
||
const combinedText = targetDocs
|
||
.map((d) => `--- ${d.filename} ---\n${String(d.extracted_text).slice(0, perDocLimit)}`)
|
||
.join('\n\n');
|
||
|
||
const analysisInput = `Aşağıdaki belgeler dava dosyasına aittir:\n\n${combinedText}`;
|
||
|
||
const { summaryJson, aiModel } = await runMediationAnalysis(analysisInput, nameToUse);
|
||
|
||
const { data: analysisRecord, error: analysisError } = await supabase
|
||
.from('analyses')
|
||
.insert([{ case_id: caseId, document_id: null, summary_json: { mediationAnalysis: summaryJson, clientName: nameToUse }, analysis_type: 'mediation', model_used: aiModel }])
|
||
.select()
|
||
.single();
|
||
if (analysisError) throw analysisError;
|
||
|
||
res.json({ analysis: analysisRecord });
|
||
} catch (error: any) {
|
||
console.error('Analyze Mediation 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' });
|
||
}
|
||
};
|
||
|
||
export const analyzeDigitalIntern = async (req: AuthenticatedRequest, res: Response) => {
|
||
try {
|
||
const { caseId } = req.params;
|
||
const { question, query } = req.body || {};
|
||
const questionToUse = question || query;
|
||
|
||
if (!caseId) {
|
||
return res.status(400).json({ error: 'Geçersiz caseId' });
|
||
}
|
||
|
||
if (!questionToUse || !String(questionToUse).trim()) {
|
||
return res.status(400).json({ error: 'Soru (question) girmelisiniz.' });
|
||
}
|
||
|
||
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 targetDocs = (docs || []).filter(d => d.extracted_text && String(d.extracted_text).trim());
|
||
|
||
if (targetDocs.length === 0) {
|
||
return res.status(400).json({ error: 'Bu dosyada arama yapılabilecek evrak bulunamadı.' });
|
||
}
|
||
|
||
const perDocLimit = Math.max(1000, Math.floor(30000 / targetDocs.length));
|
||
const combinedText = targetDocs
|
||
.map((d) => `--- ${d.filename} ---\n${String(d.extracted_text).slice(0, perDocLimit)}`)
|
||
.join('\n\n');
|
||
|
||
const analysisInput = `Aşağıdaki belgeler dava dosyasına aittir:\n\n${combinedText}`;
|
||
|
||
const { summaryJson, aiModel } = await runDigitalInternQa(analysisInput, questionToUse);
|
||
const userId = req.user?.id;
|
||
|
||
// Dijital Stajyer sonucunu hem 'analyses' hem de 'chat_messages' tablosuna kaydet
|
||
await supabase
|
||
.from('analyses')
|
||
.insert([{ case_id: caseId, document_id: null, summary_json: { digitalInternResult: summaryJson, question: questionToUse }, model_used: aiModel }]);
|
||
|
||
if (userId) {
|
||
await supabase.from('chat_messages').insert([
|
||
{
|
||
case_id: caseId,
|
||
user_id: userId,
|
||
role: 'assistant',
|
||
content: JSON.stringify(summaryJson),
|
||
chat_mode: 'intern'
|
||
}
|
||
]);
|
||
}
|
||
|
||
res.json({ result: summaryJson, modelUsed: aiModel });
|
||
} catch (error: any) {
|
||
console.error('Digital Intern Error:', error);
|
||
res.status(500).json({ error: error.message || 'Internal Server Error' });
|
||
}
|
||
};
|