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' }); } };