223 lines
9.6 KiB
JavaScript
223 lines
9.6 KiB
JavaScript
import { quantityParts, parseBidPrice, bidRowTotal, validateBidRows } from './bidValidation.js';
|
||
/**
|
||
* İ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 toplamKurus = Math.round((mutlak + Number.EPSILON) * 100);
|
||
const tam = Math.floor(toplamKurus / 100);
|
||
const kurus = toplamKurus % 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 const miktarAyristir = quantityParts;
|
||
|
||
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; }
|
||
thead { display: table-header-group; }
|
||
tr { break-inside: avoid; }
|
||
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, existingWindow) {
|
||
const w = existingWindow || 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.');
|
||
if(w.closed) throw new Error('Çıktı penceresi kapatıldı. Yeniden deneyin.');
|
||
w.document.open();
|
||
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 || '[DOĞRULAYIN]')}</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 errors = validateBidRows(satirlar);
|
||
if (errors.length) throw new Error(errors.join(" "));
|
||
const govde = satirlar.map((r, i) => {
|
||
const { miktar, birim } = miktarAyristir(r.quantity);
|
||
const bf = parseBidPrice(r.birimFiyat);
|
||
const tutar = bidRowTotal(r);
|
||
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 ? bf.toLocaleString('tr-TR',{minimumFractionDigits:2,maximumFractionDigits:8}) : '—'}</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>`;
|
||
}
|