Files
ihaleist/src/components/DeepAnalysisReport.jsx
T

352 lines
18 KiB
React
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 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 &amp; 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>
);
};