feat: OKAS kod aramasi, toplu ihale dokumani yukleme, kazanan istihbarati
Uc ozellik, 27+1 depo taramasinda gorulen fikirlerden; kod kopyalanmadi
(ilgili depolar lisanssiz), uygulamalar sifirdan yazildi.
1) OKAS koduyla arama (fikir: firatbadur/ihaletakip-api'nin kullandigi
IhtiyacKalemleri/GetAll ucu). EKAP'in kendi OKAS katalogunda ad ile
(TR/EN) arama: "ortoped" → 33141700 Ortopedik malzemeler, 33183000
Ortopedik destek cihazlari. Secilen kodla EKAP'ta ihale aramasi
(okasBransKodList). Olculen: 33141700 altinda 335 ihale, basliklari
"VETERINER ILAC VE SARF MALZEME" — basliktan gorunmeyen turden.
- ekap-v2.mjs: okasAra(), searchByOkas(); sonuc normalize ortak
- GET /api/okas/ara?q= (1 sa onbellek); /api/urun-ara?okas=kod,kod
- Urun Arama modalinda "OKAS koduyla" modu (katalog onerisi + cok kod)
- Sol menude CPV bolumunun ustunde OKAS kutusu: kod secilince modal
o kodla acilip hemen arar
2) Toplu ihale dokumani yukleme (fikir: barkem-tayyar/ihale'nin izlenen
klasor + extractIkn deseni). Belge Kasasi'na coklu dosya secimi: her
dosyanin IKN'si METNINDEN bulunur ("Ihale Kayit Numarasi (IKN):
2026/1166496"), kullaniciya sorulmaz; ihaleye baglanir ve teklif
cetveli kalemleri otomatik havuza yazilir. Onlarca sartnameyi tek
seferde atarak kalem havuzunu hizla doldurmak icin.
- kalem-cikar.mjs#iknCikar + istemci esi (PDF tarayicida okunuyor)
- Yukleme ucu: otoIkn → ofis metninden IKN; donuste ikn alani
- Dosya basina sonuc satiri: IKN / kalem sayisi / "IKN bulunamadi"
Dogrulandi: ikn parametresiz docx → 2026/1588948 bulundu, 2 kalem.
3) Kazanan istihbarati (fikir: gulmezeren2-byte/ihalent, MIT). Rakip
Radari'nda firma basina kirim orani (1 − sozlesme/yaklasik maliyet,
ortalama), ortalama gecerli teklif sayisi (rekabet derinligi) ve tek
teklifle kazanma orani. Veri yoksa null — 0 ile karismasin; bedel >
maliyet×1,5 olan bozuk satirlar orana katilmiyor.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { listDocuments, uploadDocument, deleteDocument, downloadDocument, formatBayt } from '../../services/account';
|
||||
import { FolderKanban, Upload, Trash2, Download, Loader2, AlertTriangle, FileText, HardDrive } from 'lucide-react';
|
||||
import { FolderKanban, Upload, Trash2, Download, Loader2, AlertTriangle, FileText, HardDrive, Layers, Package, CheckCircle2, XCircle } from 'lucide-react';
|
||||
|
||||
const KATEGORILER = ['Yasal Belgeler', 'Kalite Belgeleri', 'Mali Belgeler', 'Resmi Kayıtlar', 'Teknik Belgeler', 'Diğer'];
|
||||
|
||||
@@ -38,6 +38,50 @@ export const DocumentVault = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Toplu ihale dokümanı: her dosyanın İKN'si METNİNDEN bulunur, kullanıcıya
|
||||
// sorulmaz; kalemler otomatik havuza yazılır. docx/xlsx/zip sunucuda,
|
||||
// PDF tarayıcıda (pdfjs) okunur. Onlarca şartnameyi tek seferde atmak için.
|
||||
const topluRef = useRef(null);
|
||||
const [toplu, setToplu] = useState([]); // [{ad, durum, ikn, kalem, hata}]
|
||||
const topluSecildi = async (e) => {
|
||||
const dosyalar = [...(e.target.files || [])];
|
||||
if (!dosyalar.length) return;
|
||||
setBusy(true); setHata(null);
|
||||
setToplu(dosyalar.map((f) => ({ ad: f.name, durum: 'bekliyor' })));
|
||||
const guncelle = (i, veri) => setToplu((o) => o.map((x, j) => (j === i ? { ...x, ...veri } : x)));
|
||||
for (let i = 0; i < dosyalar.length; i++) {
|
||||
const f = dosyalar[i];
|
||||
guncelle(i, { durum: 'yukleniyor' });
|
||||
try {
|
||||
if (f.size > 10 * 1024 * 1024) throw new Error('10 MB sınırı');
|
||||
const pdfGibi = /\.(pdf|txt|md)$/i.test(f.name);
|
||||
let ikn = null;
|
||||
let metin = null;
|
||||
if (pdfGibi) {
|
||||
const { extractText } = await import('../../services/pdfText');
|
||||
const { iknCikar } = await import('../../services/kalem');
|
||||
metin = (await extractText(f)).text;
|
||||
ikn = iknCikar(metin);
|
||||
}
|
||||
const cevap = await uploadDocument(f, 'İhale Dokümanı', ikn, { otoIkn: true });
|
||||
ikn = cevap?.ikn || ikn;
|
||||
let kalem = cevap?.kalem?.sayi || null;
|
||||
if (ikn && !kalem && metin) {
|
||||
try {
|
||||
const { kalemCikar } = await import('../../services/kalem');
|
||||
kalem = (await kalemCikar({ ikn, metin, dosya: f.name })).sayi || null;
|
||||
} catch { /* kalem çıkmazsa yükleme yine başarılı */ }
|
||||
}
|
||||
guncelle(i, { durum: 'tamam', ikn, kalem });
|
||||
setDurum(cevap);
|
||||
} catch (err) {
|
||||
guncelle(i, { durum: 'hata', hata: err.message });
|
||||
}
|
||||
}
|
||||
setBusy(false);
|
||||
if (topluRef.current) topluRef.current.value = '';
|
||||
};
|
||||
|
||||
const sil = async (id) => {
|
||||
setBusy(true); setHata(null);
|
||||
try { setDurum(await deleteDocument(id)); }
|
||||
@@ -110,6 +154,51 @@ export const DocumentVault = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toplu ihale dokümanı yükleme */}
|
||||
<div className="bg-linear-to-r from-slate-900 via-indigo-950 to-slate-900 rounded-2xl p-5 space-y-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-amber-400 text-slate-950 flex items-center justify-center shrink-0">
|
||||
<Layers className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h3 className="font-black text-sm text-white">İhale Dokümanlarını Toplu Yükle</h3>
|
||||
<p className="text-[11px] text-slate-300 leading-relaxed">
|
||||
EKAP'tan indirdiğiniz şartname/doküman dosyalarını (PDF, DOCX, XLSX, ZIP) hepsini birden seçin.
|
||||
Her dosyanın <strong className="text-white">İKN'si metninden bulunur</strong>, ihaleye bağlanır ve
|
||||
teklif cetveli kalemleri otomatik çıkarılır — o ihale herkesin kartında kalemleriyle görünür.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<label className={`inline-flex font-black text-xs px-5 py-2.5 rounded-xl items-center gap-2 ${
|
||||
busy ? 'bg-slate-600 text-slate-300 cursor-not-allowed' : 'bg-amber-400 hover:bg-amber-300 text-slate-950 cursor-pointer'
|
||||
}`}>
|
||||
{busy ? <Loader2 className="w-4 h-4 animate-spin" /> : <Upload className="w-4 h-4" />}
|
||||
{busy ? 'İşleniyor…' : 'Dosyaları Seç (çoklu)'}
|
||||
<input ref={topluRef} type="file" multiple accept=".pdf,.docx,.xlsx,.zip,.txt" className="hidden" disabled={busy} onChange={topluSecildi} />
|
||||
</label>
|
||||
{toplu.length > 0 && (
|
||||
<ul className="space-y-1">
|
||||
{toplu.map((t, i) => (
|
||||
<li key={i} className="bg-black/30 rounded-lg px-3 py-2 flex items-center gap-2 text-[11px]">
|
||||
{t.durum === 'tamam' ? <CheckCircle2 className="w-3.5 h-3.5 text-emerald-400 shrink-0" />
|
||||
: t.durum === 'hata' ? <XCircle className="w-3.5 h-3.5 text-red-400 shrink-0" />
|
||||
: <Loader2 className={`w-3.5 h-3.5 text-slate-400 shrink-0 ${t.durum === 'yukleniyor' ? 'animate-spin' : ''}`} />}
|
||||
<span className="text-slate-200 truncate flex-1">{t.ad}</span>
|
||||
{t.durum === 'tamam' && (
|
||||
<span className="shrink-0 flex items-center gap-2">
|
||||
{t.ikn
|
||||
? <span className="font-mono font-bold text-amber-300">{t.ikn}</span>
|
||||
: <span className="text-amber-200/80">İKN bulunamadı — ihaleye bağlanmadı</span>}
|
||||
{t.kalem ? <span className="text-emerald-300 font-bold flex items-center gap-1"><Package className="w-3 h-3" />{t.kalem} kalem</span> : null}
|
||||
</span>
|
||||
)}
|
||||
{t.durum === 'hata' && <span className="text-red-300 shrink-0">{t.hata}</span>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hata && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-4 flex gap-3">
|
||||
<AlertTriangle className="w-4 h-4 text-red-600 shrink-0 mt-0.5" />
|
||||
|
||||
Reference in New Issue
Block a user