352 lines
18 KiB
React
352 lines
18 KiB
React
import React, { useRef } from 'react';
|
||
import { useApp } from '../context/AppContext';
|
||
import { bidSourceSnapshot } from '../services/bidSource';
|
||
import { documentKey } from '../services/preparationChecks';
|
||
import { YeterlilikRozeti } from './YeterlilikRozeti';
|
||
import {
|
||
AlertOctagon, AlertTriangle, Eye, CalendarClock, ClipboardList, Route,
|
||
HelpCircle, Coins, Target, 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 evidenceLabel = path => { const [field,index] = path.split('.'); return `${({itemsAndSpecs:'Kalem',documentChecklist:'Belge',qualificationCriteria:'Yeterlik',criticalRequirements:'Kritik koşul',risksAndPenalties:'Risk',criticalDetails:'Kritik ayrıntı',disqualificationRisks:'Eleme riski',deadlines:'Süre',roadmap:'Hazırlık adımı'})[field] || 'Bulgu'} ${Number(index)+1}`; };
|
||
|
||
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: selectedTender }) => {
|
||
const {tenders, mutateTender, savingCount, saveError} = useApp();
|
||
const preparationRef = useRef(null);
|
||
const tender = {...selectedTender, ...tenders.find(t => t.id === selectedTender?.id)};
|
||
const sourceSnapshot = bidSourceSnapshot(tender, results);
|
||
const savedSteps = Array.isArray(tender.checklist) ? tender.checklist : [];
|
||
const stepDone = key => savedSteps.some(s => s.key === key && s.sourceSnapshot === sourceSnapshot && s.done);
|
||
const toggleStep = key => mutateTender(tender.id, {checklist:[
|
||
...savedSteps.filter(s => s.key !== key), {key, sourceSnapshot, done:!stepDone(key)},
|
||
]});
|
||
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;
|
||
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{results.sourceReview && <div className="border border-amber-300 bg-amber-50 rounded-xl p-3 text-xs space-y-1">
|
||
<p className="font-bold">{results.sourceReview.status === 'partial' ? 'Kısmi analiz — kontrol gerekli' : 'Analiz üretildi — kaynak doğrulaması gerekli'}</p>
|
||
<p>Kaynak: {results.sourceReview.fileName || 'Gönderilen metin'} · {results.sourceReview.analyzedCharacters} / {results.sourceReview.sourceCharacters} karakter işlendi</p>
|
||
{(results.sourceReview.issues || []).map(issue=><p key={issue} role="alert">{issue}</p>)}
|
||
{results.sourceReview.parts && <p>İşlenen bölümler: {results.sourceReview.parts.filter(p=>p.status==='processed').length} / {results.sourceReview.parts.length}</p>}
|
||
<p>Boş bir bölüm, ilgili şartın kaynakta bulunmadığı anlamına gelmez.</p>
|
||
</div>}
|
||
|
||
{results.sourceReview?.unverifiedFindings?.length > 0 && <p role="status" className="text-xs text-amber-800">
|
||
{results.sourceReview.unverifiedFindings.length} bulgunun tekil kaynak alıntısı doğrulanamadı. Resmi metinden kontrol edin.
|
||
</p>}
|
||
{results.sourceEvidence?.length > 0 && <details className="border rounded-xl p-3 text-xs space-y-2">
|
||
<summary className="font-bold cursor-pointer">Kaynak alıntıları ({results.sourceEvidence.length})</summary>
|
||
<p>Metinde bulunması, modelin yorumunun doğru olduğunu tek başına kanıtlamaz.</p>
|
||
{results.sourceEvidence.map((e,i)=><div key={i} className="border-t pt-2">
|
||
<p className="font-semibold">{evidenceLabel(e.path)} — {e.status==='located'?'Alıntı kaynakta bulundu':e.status==='ambiguous'?'Alıntı birden fazla yerde geçiyor':'Alıntı kaynakta bulunamadı'}</p>
|
||
<blockquote className="whitespace-pre-wrap border-l-2 pl-2 mt-1">{e.quote}</blockquote>
|
||
</div>)}
|
||
</details>}
|
||
|
||
{/* İş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">
|
||
{/* Source requirements summarize the single saved preparation checklist. */}
|
||
{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">Belge Gereksinimleri</Baslik>
|
||
<p className="text-xs text-slate-500">Kontrolleri aşağıdaki hazırlık formunda kaynaklarıyla kaydedin.</p>
|
||
<ul className="space-y-1.5">
|
||
{documentChecklist.map((b, i) => {
|
||
const check = tender.bidDraft?.checks?.documents?.[documentKey(b)];
|
||
return <li key={i} className="bg-slate-50 border border-slate-200 rounded-lg p-2.5 space-y-1">
|
||
<span className="text-xs font-semibold">{b.document || '—'}</span>
|
||
<p className="text-[11px] text-slate-500">{b.note}</p>
|
||
<YeterlilikRozeti belge={b.document} />
|
||
<p className="text-[11px] text-slate-600">{check?.status === 'verified' && check?.evidence?.trim()
|
||
? 'Kayıtlı kullanıcı kontrolü var; güncel kaynak kontrolünü hazırlık formundan doğrulayın.'
|
||
: 'Belge kontrolü bekliyor.'}</p>
|
||
</li>;
|
||
})}
|
||
</ul>
|
||
<button type="button" onClick={() => {preparationRef.current?.scrollIntoView({behavior:'smooth',block:'start'});preparationRef.current?.focus({preventScroll:true});}}
|
||
className="text-xs font-bold text-blue-700">Belge ve ürün kontrollerine git</button>
|
||
</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 = JSON.stringify(a);
|
||
const secili = stepDone(k);
|
||
return (
|
||
<li key={i} className="flex gap-3">
|
||
<div className="flex flex-col items-center shrink-0">
|
||
<button
|
||
type="button"
|
||
disabled={savingCount > 0}
|
||
onClick={() => toggleStep(k)}
|
||
aria-label={`${i + 1}. adımı ${secili ? 'yeniden aç' : 'tamamlandı işaretle'}`}
|
||
aria-pressed={secili}
|
||
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 */}
|
||
{saveError && <p role="alert" className="text-xs text-red-700">{saveError}</p>}
|
||
<div ref={preparationRef} tabIndex={-1} aria-label="Kalıcı ihale hazırlığı">
|
||
<BidPackageBuilder key={tender?.ikn || tender?.id} results={results} tender={tender} />
|
||
</div>
|
||
|
||
</div>
|
||
);
|
||
};
|