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:
@@ -0,0 +1,328 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
AlertOctagon, AlertTriangle, Eye, CalendarClock, ClipboardList, Route,
|
||||
HelpCircle, Coins, Target, CheckSquare, Square, Package,
|
||||
} from 'lucide-react';
|
||||
import { BidPackageBuilder } from './BidPackageBuilder';
|
||||
|
||||
/**
|
||||
* Derin analiz raporu — /api/ai/analyze mode='derin' çıktısını gösterir.
|
||||
* Standart analizden farkı: elenme sebepleri, gözden kaçan ince detaylar,
|
||||
* belge kontrol listesi, süreler ve teklife kadarki yol haritası.
|
||||
*
|
||||
* Model alanları kısmi doldurabilir; her bölüm kendi verisi varsa çizilir.
|
||||
*/
|
||||
|
||||
const SEVIYE_RENK = {
|
||||
Kritik: 'bg-red-100 text-red-800 border-red-200',
|
||||
Yüksek: 'bg-orange-100 text-orange-800 border-orange-200',
|
||||
Orta: 'bg-amber-100 text-amber-800 border-amber-200',
|
||||
Düşük: 'bg-slate-200 text-slate-700 border-slate-300',
|
||||
};
|
||||
|
||||
const FAZ_RENK = {
|
||||
Hazırlık: 'bg-slate-700',
|
||||
'Belge Toplama': 'bg-blue-600',
|
||||
Teknik: 'bg-indigo-600',
|
||||
Fiyatlandırma: 'bg-emerald-600',
|
||||
Teklif: 'bg-amber-600',
|
||||
Sözleşme: 'bg-purple-600',
|
||||
};
|
||||
|
||||
const dolu = (x) => Array.isArray(x) && x.length > 0;
|
||||
|
||||
const tl = (n) =>
|
||||
typeof n === 'number' && isFinite(n)
|
||||
? n.toLocaleString('tr-TR', { maximumFractionDigits: 2 }) + ' ₺'
|
||||
: null;
|
||||
|
||||
const Baslik = ({ icon: Icon, renk, children, not }) => (
|
||||
<h4 className="font-extrabold text-sm text-slate-900 flex items-center gap-2 border-b border-slate-100 pb-2">
|
||||
<Icon className={`w-4 h-4 ${renk}`} />
|
||||
<span>{children}</span>
|
||||
{not && <span className="ml-auto text-[10px] font-semibold text-slate-400">{not}</span>}
|
||||
</h4>
|
||||
);
|
||||
|
||||
export const DeepAnalysisReport = ({ results, tender }) => {
|
||||
const [isaretli, setIsaretli] = useState({});
|
||||
if (!results) return null;
|
||||
|
||||
const {
|
||||
scopeOfWork, disqualificationRisks, criticalDetails, documentChecklist,
|
||||
deadlines, costDrivers, roadmap, questionsForAdministration, itemsAndSpecs,
|
||||
} = results;
|
||||
|
||||
const varMi = scopeOfWork || dolu(disqualificationRisks) || dolu(criticalDetails) ||
|
||||
dolu(documentChecklist) || dolu(deadlines) || dolu(costDrivers) ||
|
||||
dolu(roadmap) || dolu(questionsForAdministration) || dolu(itemsAndSpecs);
|
||||
if (!varMi) return null;
|
||||
|
||||
const degistir = (k) => setIsaretli((o) => ({ ...o, [k]: !o[k] }));
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* İşin kapsamı */}
|
||||
{scopeOfWork && (
|
||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-2">
|
||||
<Baslik icon={Target} renk="text-indigo-600">İşin Kapsamı</Baslik>
|
||||
<p className="text-xs text-slate-700 leading-relaxed">{scopeOfWork}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* UYARI — teklifi geçersiz kılan noktalar */}
|
||||
{dolu(disqualificationRisks) && (
|
||||
<div className="bg-red-50 border-2 border-red-300 rounded-2xl p-5 space-y-3 shadow-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-red-600 text-white flex items-center justify-center shrink-0">
|
||||
<AlertOctagon className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-black text-sm text-red-900">Teklifinizi Geçersiz Kılabilecek Noktalar</h4>
|
||||
<p className="text-[11px] text-red-700">Bu maddelerden biri eksikse teklif değerlendirme dışı bırakılabilir.</p>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{disqualificationRisks.map((r, i) => (
|
||||
<li key={i} className="flex items-start gap-2 bg-white border border-red-200 rounded-lg p-2.5">
|
||||
<AlertTriangle className="w-3.5 h-3.5 text-red-600 shrink-0 mt-0.5" />
|
||||
<span className="text-xs text-red-900 font-medium leading-relaxed">{r}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Gözden kaçan ince detaylar */}
|
||||
{dolu(criticalDetails) && (
|
||||
<div className="bg-white p-5 rounded-2xl border border-amber-300 shadow-xs space-y-3">
|
||||
<Baslik icon={Eye} renk="text-amber-600" not={`${criticalDetails.length} madde`}>
|
||||
Gözden Kaçmaması Gereken İnce Detaylar
|
||||
</Baslik>
|
||||
<div className="space-y-2.5">
|
||||
{criticalDetails.map((d, i) => (
|
||||
<div key={i} className="bg-amber-50/60 border border-amber-200 rounded-xl p-3 space-y-1.5">
|
||||
<div className="flex items-start justify-between gap-2 flex-wrap">
|
||||
<span className="font-bold text-xs text-slate-900">{d.title || 'Detay'}</span>
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
{d.where && (
|
||||
<span className="text-[10px] font-semibold text-slate-500 bg-white border border-slate-200 px-1.5 py-0.5 rounded">
|
||||
{d.where}
|
||||
</span>
|
||||
)}
|
||||
<span className={`text-[10px] font-extrabold px-2 py-0.5 rounded border ${SEVIYE_RENK[d.level] || SEVIYE_RENK.Orta}`}>
|
||||
{d.level || 'Orta'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{d.detail && <p className="text-xs text-slate-700 leading-relaxed">{d.detail}</p>}
|
||||
{d.whyItMatters && (
|
||||
<p className="text-[11px] text-amber-900 bg-amber-100/70 rounded-lg px-2.5 py-1.5 leading-relaxed">
|
||||
<strong>Neden önemli:</strong> {d.whyItMatters}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Alınacak ürün/hizmet: evsaf, miktar, tahmini fiyat */}
|
||||
{dolu(itemsAndSpecs) && (
|
||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-3">
|
||||
<Baslik icon={Package} renk="text-emerald-600" not={`${itemsAndSpecs.length} kalem`}>
|
||||
Alınacak Kalemler & Aranan Evsaflar
|
||||
</Baslik>
|
||||
<div className="space-y-2.5">
|
||||
{itemsAndSpecs.map((k, i) => (
|
||||
<div key={i} className="bg-slate-50 border border-slate-200 rounded-xl p-3 space-y-2">
|
||||
<div className="flex items-start justify-between gap-3 flex-wrap">
|
||||
<span className="font-bold text-xs text-slate-900 flex-1 min-w-0">{k.name || '—'}</span>
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
{k.quantity && (
|
||||
<span className="text-[11px] font-black text-slate-900 bg-white border border-slate-300 px-2 py-0.5 rounded">
|
||||
{k.quantity}
|
||||
</span>
|
||||
)}
|
||||
{tl(k.estimatedUnitPrice) && (
|
||||
<span className="text-[11px] font-bold text-emerald-800 bg-emerald-100 border border-emerald-200 px-2 py-0.5 rounded"
|
||||
title="Şartnamede geçen tahmini birim fiyat">
|
||||
birim {tl(k.estimatedUnitPrice)}
|
||||
</span>
|
||||
)}
|
||||
{tl(k.estimatedTotal) && (
|
||||
<span className="text-[11px] font-black text-emerald-900 bg-emerald-200 px-2 py-0.5 rounded"
|
||||
title="Şartnamede geçen tahmini toplam">
|
||||
toplam {tl(k.estimatedTotal)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{dolu(k.keySpecs) && (
|
||||
<ul className="space-y-1">
|
||||
{k.keySpecs.map((e, j) => (
|
||||
<li key={j} className="flex items-start gap-2 text-[11px] text-slate-700 leading-relaxed">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-600 mt-1.5 shrink-0" />
|
||||
<span>{e}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{k.note && <p className="text-[11px] text-slate-500 leading-snug italic">{k.note}</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-400 leading-relaxed">
|
||||
Fiyatlar yalnızca şartnamede/ilanda geçiyorsa gösterilir; model tahmin üretmez.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Belge kontrol listesi */}
|
||||
{dolu(documentChecklist) && (
|
||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-3">
|
||||
<Baslik icon={ClipboardList} renk="text-blue-600" not="işaretleyerek ilerleyin">
|
||||
Belge Kontrol Listesi
|
||||
</Baslik>
|
||||
<ul className="space-y-1.5">
|
||||
{documentChecklist.map((b, i) => {
|
||||
const k = `b${i}`;
|
||||
const secili = !!isaretli[k];
|
||||
return (
|
||||
<li key={i}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => degistir(k)}
|
||||
className="w-full text-left flex items-start gap-2 bg-slate-50 hover:bg-slate-100 border border-slate-200 rounded-lg p-2.5 transition-colors"
|
||||
>
|
||||
{secili
|
||||
? <CheckSquare className="w-4 h-4 text-emerald-600 shrink-0 mt-0.5" />
|
||||
: <Square className="w-4 h-4 text-slate-300 shrink-0 mt-0.5" />}
|
||||
<span className="flex-1 min-w-0">
|
||||
<span className={`text-xs font-semibold block ${secili ? 'line-through text-slate-400' : 'text-slate-800'}`}>
|
||||
{b.document || '—'}
|
||||
</span>
|
||||
{b.note && <span className="text-[11px] text-slate-500 leading-snug block mt-0.5">{b.note}</span>}
|
||||
</span>
|
||||
{b.mandatory === false
|
||||
? <span className="text-[10px] font-bold text-slate-500 bg-slate-200 px-1.5 py-0.5 rounded shrink-0">İsteğe bağlı</span>
|
||||
: <span className="text-[10px] font-bold text-red-700 bg-red-100 px-1.5 py-0.5 rounded shrink-0">Zorunlu</span>}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Süreler + maliyet sürücüleri */}
|
||||
<div className="space-y-6">
|
||||
{dolu(deadlines) && (
|
||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-3">
|
||||
<Baslik icon={CalendarClock} renk="text-rose-600">Kritik Tarihler & Süreler</Baslik>
|
||||
<div className="space-y-2">
|
||||
{deadlines.map((d, i) => (
|
||||
<div key={i} className="bg-slate-50 border border-slate-200 rounded-lg p-2.5">
|
||||
<div className="flex items-baseline justify-between gap-2 flex-wrap">
|
||||
<span className="text-xs font-semibold text-slate-800">{d.label || '—'}</span>
|
||||
<span className="text-xs font-black text-rose-700">{d.value || '—'}</span>
|
||||
</div>
|
||||
{d.note && <p className="text-[11px] text-slate-500 leading-snug mt-0.5">{d.note}</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{dolu(costDrivers) && (
|
||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-3">
|
||||
<Baslik icon={Coins} renk="text-emerald-600">Maliyeti Belirleyen Kalemler</Baslik>
|
||||
<ul className="space-y-1.5 text-xs text-slate-700">
|
||||
{costDrivers.map((c, i) => (
|
||||
<li key={i} className="flex items-start gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-600 mt-1.5 shrink-0" />
|
||||
<span className="leading-relaxed">{c}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Yol haritası */}
|
||||
{dolu(roadmap) && (
|
||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-3">
|
||||
<Baslik icon={Route} renk="text-purple-600" not={`${roadmap.length} adım`}>
|
||||
Teklife Kadar Yol Haritası
|
||||
</Baslik>
|
||||
<ol className="space-y-2">
|
||||
{roadmap.map((a, i) => {
|
||||
const k = `y${i}`;
|
||||
const secili = !!isaretli[k];
|
||||
return (
|
||||
<li key={i} className="flex gap-3">
|
||||
<div className="flex flex-col items-center shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => degistir(k)}
|
||||
aria-label={`${i + 1}. adımı tamamlandı işaretle`}
|
||||
className={`w-7 h-7 rounded-full text-white text-[11px] font-black flex items-center justify-center transition-colors ${
|
||||
secili ? 'bg-emerald-600' : (FAZ_RENK[a.phase] || 'bg-slate-700')
|
||||
}`}
|
||||
>
|
||||
{secili ? '✓' : (a.step ?? i + 1)}
|
||||
</button>
|
||||
{i < roadmap.length - 1 && <div className="w-px flex-1 bg-slate-200 my-1" />}
|
||||
</div>
|
||||
<div className={`flex-1 min-w-0 bg-slate-50 border border-slate-200 rounded-xl p-3 space-y-1 mb-1 ${secili ? 'opacity-60' : ''}`}>
|
||||
<div className="flex items-start justify-between gap-2 flex-wrap">
|
||||
<span className={`text-xs font-bold ${secili ? 'line-through text-slate-400' : 'text-slate-900'}`}>
|
||||
{a.action || '—'}
|
||||
</span>
|
||||
{a.phase && (
|
||||
<span className="text-[10px] font-extrabold text-white px-2 py-0.5 rounded shrink-0 bg-slate-700">
|
||||
{a.phase}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-0.5 text-[11px] text-slate-500">
|
||||
{a.owner && <span><strong className="text-slate-600">Kim:</strong> {a.owner}</span>}
|
||||
{a.timing && <span><strong className="text-slate-600">Ne zaman:</strong> {a.timing}</span>}
|
||||
{a.output && <span><strong className="text-slate-600">Çıktı:</strong> {a.output}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* İdareye sorulacaklar */}
|
||||
{dolu(questionsForAdministration) && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-2xl p-5 space-y-3">
|
||||
<Baslik icon={HelpCircle} renk="text-blue-700">İdareye Sorulması Gereken Belirsizlikler</Baslik>
|
||||
<ul className="space-y-1.5">
|
||||
{questionsForAdministration.map((q, i) => (
|
||||
<li key={i} className="flex items-start gap-2 text-xs text-blue-900 leading-relaxed">
|
||||
<span className="font-black text-blue-500 shrink-0">{i + 1}.</span>
|
||||
<span>{q}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="text-[10px] text-blue-700 leading-relaxed">
|
||||
Bu sorular için şartnamede belirtilen süre içinde idareye açıklama talebi / zeyilname
|
||||
başvurusu yapabilirsiniz.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Analiz ve yol haritası okunduktan sonraki adım: teklif zarfını hazırla */}
|
||||
<BidPackageBuilder results={results} tender={tender} />
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user