Files
ihaleist/src/services/bidDocs.js
T

223 lines
9.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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) => ({ '&': '&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; }
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>`;
}