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:
+2
-2
@@ -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})`);
|
||||
|
||||
@@ -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) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[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>`;
|
||||
}
|
||||
Reference in New Issue
Block a user