feat: derin analiz & yol haritasi + ihale dosyasi hazirlama; docx/xlsx/zip okunuyor

Doküman panelindeki "Tum Evraki Derin Analiz Et" dugmesi EKAP ilan metnini ve
ihaleye yuklenmis butun belgeleri TEK metinde birlestirip gonderiyor; model
sartnameyi ilanla ve eklerle capraz okuyabiliyor.

Derin analiz (mode='derin', ayri sistem istemi + 8000 token):
- Teklifi gecersiz kilabilecek noktalar (kirmizi uyari, en ustte)
- Gozden kacan ince detaylar (Kritik/Yuksek/Orta + madde no + neden onemli)
- Alinacak kalemler: aranan evsaflar, miktar, sartnamede geciyorsa fiyat
  (modele "TAHMIN URETME" talimati verildi, yoksa null)
- Belge kontrol listesi, kritik tarihler, maliyet kalemleri
- Teklife kadar yol haritasi (faz / kim / ne zaman / cikti)
- Idareye sorulacak belirsizlikler
Standart hizli ozet modu aynen duruyor.

Ihale Dosyasi Hazirla (rapor sonunda, acilir bolum):
- Eksik firma bilgileri kullaniciya sorulur, profilden gelenler sorulmaz,
  girilenler tarayicida saklanir
- Kalem fiyatlandirma tablosu, canli toplam + yaziyla karsiligi
- Teklif Mektubu, Birim Fiyat Teklif Cetveli, Evrak Kontrol Listesi
- Belgeler yazdirilabilir HTML: jsPDF'in gomulu fontlari WinAnsi, s/g/i/I
  tasimiyor (bkz. services/pdf.js ASCII basliklari) — resmi teklif mektubunda
  bozuk Turkce kabul edilemez

docx/xlsx/zip metin cikarimi (server/office-text.mjs):
- ihaleab projesindeki officeText.ts'ten tasindi, harici bagimlilik yok
- Sunucuda: tarayicida cozucu yok, baytlar zaten BELGE_DIR'de
- Tablolar " | " ile korunuyor, ic ice zip aciliyor, ~$ gecici dosyalari atlanir
- Yeni uc: GET /api/me/documents/text
- Yukleme kabul listesi .doc/.rar cikarildi (karsiligi yok), .xlsx eklendi

fix: yuklenen belgelerdeki "AI analiz" dugmesi bozuktu — extractText nesne
donduruyor ({text,pageCount,usedPages}), kod onu duz metin sanip gonderiyordu;
sunucuya "[object Object]" gidiyor ve analiz reddediliyordu. Uzunluk kontrolu
de nesne uzerinde undefined verdigi icin hatayi yutuyordu.

fix: analiz dokuman panelinden baslatildiginda modal kendi isAnalyzing'ini
bilmiyordu, kullanici 1-2 dakika bos modal goruyordu — ortak aiAnalyzing sinyali
This commit is contained in:
2026-08-31 14:53:46 +03:00
parent be79f9366a
commit 3eb0f91823
10 changed files with 1584 additions and 51 deletions
+2 -2
View File
@@ -13,11 +13,11 @@ export async function getAiConfig() {
}
}
export async function analyzeSpecText({ text, fileName, tender, chain }) {
export async function analyzeSpecText({ text, fileName, tender, chain, mode }) {
const res = await fetch('/api/ai/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` },
body: JSON.stringify({ text, fileName, tender, chain }),
body: JSON.stringify({ text, fileName, tender, chain, mode }),
});
const data = await res.json().catch(() => ({}));
if (!data.success) throw new Error(data.error || `Analiz başarısız (HTTP ${res.status})`);
+219
View File
@@ -0,0 +1,219 @@
/**
* İhale dosyası belgeleri — teklif mektubu, birim fiyat teklif cetveli ve
* evrak kontrol listesi.
*
* NEDEN HTML/YAZDIR: jsPDF'in gömülü fontları WinAnsi; ş/ğ/ı/İ taşımıyor
* (bkz. services/pdf.js, başlıkları ASCII'ye düşürüyor). Resmî teklif
* mektubunda bozuk Türkçe kabul edilemez, o yüzden belgeler tam Türkçe HTML
* olarak üretilip tarayıcının yazdır → "PDF olarak kaydet" akışına veriliyor.
*/
const BIRLER = ['', 'bir', 'iki', 'üç', 'dört', 'beş', 'altı', 'yedi', 'sekiz', 'dokuz'];
const ONLAR = ['', 'on', 'yirmi', 'otuz', 'kırk', 'elli', 'altmış', 'yetmiş', 'seksen', 'doksan'];
const BASAMAK = ['', 'bin', 'milyon', 'milyar', 'trilyon'];
function ucBasamak(n) {
const y = Math.floor(n / 100), o = Math.floor((n % 100) / 10), b = n % 10;
let s = '';
if (y) s += y === 1 ? 'yüz' : BIRLER[y] + 'yüz';
if (o) s += ONLAR[o];
if (b) s += BIRLER[b];
return s;
}
/** 1850000.5 → "birmilyonsekizyüzellibin Türk Lirası elli Kuruş" */
export function yaziylaTL(n) {
const sayi = Number(n);
if (!isFinite(sayi)) return '';
const negatif = sayi < 0;
const mutlak = Math.abs(sayi);
const tam = Math.floor(mutlak);
const kurus = Math.round((mutlak - tam) * 100);
const gruplar = [];
let kalan = tam;
while (kalan > 0) { gruplar.push(kalan % 1000); kalan = Math.floor(kalan / 1000); }
let lira = '';
for (let i = gruplar.length - 1; i >= 0; i--) {
const g = gruplar[i];
if (!g) continue;
// "birbin" değil "bin"
lira += (i === 1 && g === 1) ? 'bin' : ucBasamak(g) + BASAMAK[i];
}
if (!lira) lira = 'sıfır';
let s = `${lira} Türk Lirası`;
if (kurus > 0) s += ` ${ucBasamak(kurus)} Kuruş`;
return (negatif ? 'eksi ' : '') + s;
}
export const paraTR = (n) =>
(Number(n) || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
/** "3 adet" → { miktar: 3, birim: "adet" } ; okunamazsa miktar null */
export function miktarAyristir(q) {
const m = /(-?[\d.,]+)\s*(.*)$/.exec(String(q || '').trim());
if (!m) return { miktar: null, birim: String(q || '').trim() };
const sayi = parseFloat(m[1].replace(/\./g, '').replace(',', '.'));
return { miktar: isFinite(sayi) ? sayi : null, birim: (m[2] || '').trim() };
}
const kacir = (s) =>
String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
const STIL = `
@page { size: A4; margin: 20mm 18mm; }
* { box-sizing: border-box; }
body { font-family: "Times New Roman", Georgia, serif; font-size: 11.5pt; line-height: 1.55; color: #000; }
h1 { font-size: 13pt; text-align: center; margin: 0 0 4mm; letter-spacing: .3px; }
h2 { font-size: 11pt; margin: 6mm 0 2mm; }
.ust { font-size: 10pt; margin-bottom: 6mm; }
.ust div { margin: .6mm 0; }
table { width: 100%; border-collapse: collapse; margin: 3mm 0; font-size: 9.5pt; }
th, td { border: 1px solid #444; padding: 1.6mm 2mm; text-align: left; vertical-align: top; }
th { background: #eee; font-weight: bold; }
td.s, th.s { text-align: right; white-space: nowrap; }
.toplam td { font-weight: bold; background: #f4f4f4; }
.imza { margin-top: 12mm; width: 70mm; margin-left: auto; text-align: center; font-size: 10pt; }
.imza .kutu { border: 1px solid #666; height: 26mm; margin-bottom: 2mm; }
.not { font-size: 8.5pt; color: #555; margin-top: 8mm; }
ul { margin: 2mm 0; padding-left: 6mm; }
li { margin: 1.2mm 0; }
.kutucuk { display: inline-block; width: 3.6mm; height: 3.6mm; border: 1px solid #333; margin-right: 2mm; vertical-align: -0.4mm; }
@media print { .yazdirma { display: none; } }
`;
/** Belgeyi kendi penceresinde açıp yazdırma diyaloğunu tetikler. */
export function belgeYazdir(baslik, govde) {
const w = window.open('', '_blank', 'width=900,height=1000');
if (!w) throw new Error('Tarayıcı yeni pencereyi engelledi. Açılır pencere iznini verip tekrar deneyin.');
w.document.write(`<!doctype html><html lang="tr"><head><meta charset="utf-8">
<title>${kacir(baslik)}</title><style>${STIL}</style></head><body>${govde}
<div class="yazdirma" style="text-align:center;margin-top:10mm">
<button onclick="window.print()" style="font:600 14px system-ui;padding:10px 22px;cursor:pointer">
Yazdır / PDF olarak kaydet
</button>
</div></body></html>`);
w.document.close();
w.focus();
return w;
}
function ustBilgi(f) {
return `<div class="ust">
<div><strong>İdarenin adı:</strong> ${kacir(f.idare)}</div>
<div><strong>İşin adı:</strong> ${kacir(f.isinAdi)}</div>
<div><strong>İhale kayıt numarası:</strong> ${kacir(f.ikn)}</div>
<div><strong>İhale tarihi:</strong> ${kacir(f.ihaleTarihi || '—')}</div>
</div>`;
}
/** KİK birim fiyat / götürü bedel teklif mektubu metni. */
export function teklifMektubuHtml(f, toplam) {
const goturu = f.teklifTuru === 'goturu';
const tur = goturu ? 'GÖTÜRÜ BEDEL TEKLİF MEKTUBU' : 'BİRİM FİYAT TEKLİF MEKTUBU';
const bedelCumle = goturu
? 'işin tamamını teklif ettiğimiz toplam bedel karşılığında yapmayı'
: 'işi, teklif birim fiyat cetvelinde belirttiğimiz birim fiyatlar üzerinden yapmayı';
return `<h1>${tur}</h1>
${ustBilgi(f)}
<p>Yukarıda ihale kayıt numarası ve adı yer alan ihaleye ilişkin ihale dokümanını
oluşturan bütün belgeler tarafımızdan incelenmiş, okunmuş ve herhangi bir ayrım ve
sınırlama yapılmadan bütün koşullarıyla kabul edilmiştir.</p>
<p>İhale dokümanında yer alan bütün şartları kabul ederek, söz konusu ${bedelCumle}
kabul ve taahhüt ediyoruz.</p>
<p>Teklifimizin, ihale tarihinden itibaren <strong>${kacir(f.gecerlilikGun || '60')}</strong>
takvim günü süreyle geçerli olduğunu beyan ediyoruz.</p>
${f.kisim ? `<p>Teklifimiz <strong>${kacir(f.kisim)}</strong> kısmına/kısımlarına ilişkindir.</p>` : ''}
<p>Teklif ettiğimiz toplam bedel:</p>
<table>
<tr><th style="width:32%">Rakam ile</th><td class="s"><strong>${paraTR(toplam)} TL</strong></td></tr>
<tr><th>Yazı ile</th><td>${kacir(yaziylaTL(toplam))}</td></tr>
</table>
<p style="font-size:9.5pt;color:#444">(Teklif edilen bedele KDV dâhil değildir.)</p>
<h2>İstekliye ait bilgiler</h2>
<table>
<tr><th style="width:32%">Adı / Ticaret unvanı</th><td>${kacir(f.firmaUnvan)}</td></tr>
<tr><th>Tebligata esas adres</th><td>${kacir(f.adres)}</td></tr>
<tr><th>Vergi dairesi / numarası</th><td>${kacir(f.vergiDairesi)} / ${kacir(f.vergiNo)}</td></tr>
<tr><th>Ticaret sicil / MERSİS no</th><td>${kacir(f.sicilNo)}</td></tr>
<tr><th>Telefon / E-posta</th><td>${kacir(f.telefon)} · ${kacir(f.eposta)}</td></tr>
</table>
<div class="imza">
<div class="kutu"></div>
<div><strong>${kacir(f.yetkiliAd)}</strong></div>
<div>${kacir(f.yetkiliGorev)}</div>
<div style="margin-top:2mm">Kaşe ve İmza</div>
</div>
<p class="not">Bu taslak Ihaleist tarafından hazırlanmıştır. İmzalamadan önce idarenin
ihale dokümanındaki standart form metniyle karşılaştırınız.</p>`;
}
/** Birim fiyat teklif cetveli. */
export function teklifCetveliHtml(f, satirlar, toplam) {
const govde = satirlar.map((r, i) => {
const { miktar, birim } = miktarAyristir(r.quantity);
const bf = Number(r.birimFiyat) || 0;
const tutar = (miktar ?? 0) * bf;
return `<tr>
<td class="s">${i + 1}</td>
<td>${kacir(r.name)}</td>
<td class="s">${miktar ?? kacir(r.quantity || '—')}</td>
<td>${kacir(birim)}</td>
<td class="s">${bf ? paraTR(bf) : '—'}</td>
<td class="s">${tutar ? paraTR(tutar) : '—'}</td>
</tr>`;
}).join('');
return `<h1>BİRİM FİYAT TEKLİF CETVELİ</h1>
${ustBilgi(f)}
<table>
<thead><tr>
<th class="s" style="width:8%">Sıra</th><th>Mal / hizmet adı</th>
<th class="s" style="width:12%">Miktar</th><th style="width:12%">Birim</th>
<th class="s" style="width:16%">Birim fiyat (TL)</th><th class="s" style="width:18%">Tutar (TL)</th>
</tr></thead>
<tbody>${govde}</tbody>
<tfoot><tr class="toplam">
<td colspan="5" style="text-align:right">Toplam tutar (KDV hariç)</td>
<td class="s">${paraTR(toplam)}</td>
</tr></tfoot>
</table>
<p>Yazı ile: ${kacir(yaziylaTL(toplam))}</p>
<div class="imza">
<div class="kutu"></div>
<div><strong>${kacir(f.firmaUnvan)}</strong></div>
<div>${kacir(f.yetkiliAd)} · ${kacir(f.yetkiliGorev)}</div>
<div style="margin-top:2mm">Kaşe ve İmza</div>
</div>`;
}
/** Teklif zarfına girecek evrakın kontrol listesi / dosya kapağı. */
export function evrakListesiHtml(f, belgeler) {
const satirlar = belgeler.map((b) => `<li>
<span class="kutucuk"></span><strong>${kacir(b.document)}</strong>
${b.mandatory === false ? ' <em>(isteğe bağlı)</em>' : ''}
${b.note ? `<div style="font-size:9pt;color:#444;margin-left:5.6mm">${kacir(b.note)}</div>` : ''}
</li>`).join('');
return `<h1>TEKLİF DOSYASI EVRAK KONTROL LİSTESİ</h1>
${ustBilgi(f)}
<div class="ust"><div><strong>İstekli:</strong> ${kacir(f.firmaUnvan)}</div></div>
<h2>Zarfa konulacak belgeler</h2>
<ul>${satirlar || '<li>Analizden belge listesi çıkarılamadı.</li>'}</ul>
<div class="imza">
<div class="kutu"></div>
<div>Hazırlayan: <strong>${kacir(f.yetkiliAd)}</strong></div>
<div style="margin-top:2mm">Tarih / İmza</div>
</div>
<p class="not">Liste, yüklediğiniz ihale dokümanının yapay zekâ analizinden üretilmiştir;
zarfı kapatmadan önce idari şartnamenin yeterlik maddeleriyle karşılaştırınız.</p>`;
}