Add saved bid preparation overview with actionable missing checks

This commit is contained in:
2026-09-06 13:26:29 +03:00
parent 2e98e5ad4f
commit 30ce6e3a6c
9 changed files with 168 additions and 13 deletions
+8 -12
View File
@@ -1,3 +1,4 @@
import { bidFirmFields as ALANLAR } from '../services/bidFirmFields';
import { preparationAnalysisSource, preparationConditionSections, conditionText } from '../services/preparationAnalysis';
import { preparationDeadlineProblem } from '../services/preparationDeadline';
import { bidValidityForForm, bidTypeForForm } from '../services/bidTerms';
@@ -29,23 +30,13 @@ import {
* Firma bilgileri localStorage'da tutulur — her ihalede yeniden sorulmasın.
*/
const ALANLAR = [
{ k: 'firmaUnvan', l: 'Firma ticaret unvanı', zorunlu: true, genis: true },
{ k: 'adres', l: 'Tebligata esas adres', zorunlu: true, genis: true },
{ k: 'vergiDairesi', l: 'Vergi dairesi', zorunlu: true },
{ k: 'vergiNo', l: 'Vergi numarası', zorunlu: true },
{ k: 'sicilNo', l: 'Ticaret sicil / MERSİS no', zorunlu: false },
{ k: 'yetkiliAd', l: 'Yetkili ad soyad', zorunlu: true },
{ k: 'yetkiliGorev', l: 'Yetkilinin görevi', zorunlu: true, ph: 'Örn. Genel Müdür' },
{ k: 'telefon', l: 'Telefon', zorunlu: true },
{ k: 'eposta', l: 'E-posta', zorunlu: true },
];
const FIRMA_ANAHTAR = 'ihaleist_firma_bilgi';
const bosFirma = () => Object.fromEntries(ALANLAR.map((a) => [a.k, '']));
export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
export const BidPackageBuilder = ({ results, tender: selectedTender, initialFocus }) => {
const { userProfile, mutateTender, savingCount, authUser, tenders } = useApp();
const tender = {...selectedTender, ...tenders.find(t => t.id === selectedTender?.id)};
const storedDraft = tender?.bidDraft || {};
@@ -67,6 +58,11 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
target.scrollIntoView({block:'center'});
target.focus({preventScroll:true});
};
useEffect(()=>{
if(!initialFocus)return;
const frame=requestAnimationFrame(()=>goToMissing(initialFocus));
return ()=>cancelAnimationFrame(frame);
},[initialFocus]);
const [dirty, setDirty] = useState(false);
const [acik, setAcik] = useState(true);
const [hata, setHata] = useState(null);
+2
View File
@@ -1,3 +1,4 @@
import { PreparationOverview } from './PreparationOverview';
import { useDeadlineClock } from '../hooks/useDeadlineClock';
import { deadlineInfo } from '../services/tenderCalendar';
import React, { useState } from 'react';
@@ -91,6 +92,7 @@ export const KanbanBoard = () => {
{savingCount > 0 && <p role="status" className="text-sm text-blue-800">Değişiklik kaydediliyor…</p>}
{saveError && <p role="alert" className="text-sm text-red-700">{saveError}</p>}
<PreparationOverview tenders={filteredTenders} now={now}/>
{/* Kanban Columns Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-3.5 overflow-x-auto pb-4">
{columns.map(col => {
+46
View File
@@ -0,0 +1,46 @@
import React,{useEffect,useState} from 'react';
import {listDocuments} from '../services/account';
import {preparationSummary} from '../services/preparationSummary';
import {useApp} from '../context/AppContext';
import {getToken} from '../services/auth';
export function PreparationOverview({tenders,now}) {
const {authUser}=useApp();
return <Overview key={`${authUser?.email}:${getToken()}`} tenders={tenders} now={now}/>;
}
function Overview({tenders,now}) {
const {setActiveTenderDetail}=useApp();
const [vault,setVault]=useState({loading:true,error:null,documents:[]}),[refresh,setRefresh]=useState(0);
useEffect(()=>{
let cancelled=false;
setVault({loading:true,error:null,documents:[]});
listDocuments().then(data=>{if(!cancelled)setVault({loading:false,error:null,documents:data.belgeler});})
.catch(error=>{if(!cancelled)setVault({loading:false,error:error.message,documents:[]});});
return()=>{cancelled=true;};
},[refresh]);
const open=(tender,target)=>setActiveTenderDetail({...tender,preparationTarget:target});
return <section aria-label="Hazırlık dosyaları özeti" className="bg-white border rounded-xl p-4 space-y-3">
<h2 className="font-bold text-lg">Hazırlık dosyaları · {tenders.length}</h2>
<p className="text-xs text-slate-600">Panodaki ve sorumlu filtresindeki kayıtlı taslaklar gösterilir. Kaydetmediğiniz değişiklikler bu özete girmez. Eksik sayısı başvuru onayı değildir; çıktı sırasında belgeler yeniden kontrol edilir.</p>
<button className="border rounded px-3 py-2 text-xs" disabled={vault.loading} onClick={()=>setRefresh(n=>n+1)}>Belge durumunu yenile</button>
{vault.loading&&<p role="status" className="text-xs">Belge durumu kontrol ediliyor…</p>}
{vault.error&&<p role="alert" className="text-xs text-red-700">Belge durumu doğrulanamadı: {vault.error}</p>}
{!tenders.length ? <p className="text-sm">İhaleyi takibe alın veya hazırlık görevi açın; dosyası burada görünsün.</p> :
<div className="overflow-x-auto"><table className="w-full text-sm min-w-[700px]">
<thead><tr className="text-left border-b"><th className="p-2">İhale</th><th className="p-2">Sorumlu</th><th className="p-2">Belge eksikleri</th><th className="p-2">Sıradaki kontrol</th></tr></thead>
<tbody>{tenders.map(tender=>{
const summary=preparationSummary(tender,vault,now);
return <tr key={tender.id} className="border-b align-top">
<td className="p-2"><button onClick={()=>open(tender,'source')} className="text-blue-800 underline text-left">{tender.ikn} · {tender.baslik}</button><p className="text-xs mt-1">{summary.issues.length ? `${summary.issues.length} kontrol eksik` : 'Kayıtlı kontroller tamam; son kontrol gerekli'}</p></td>
<td className="p-2">{summary.owner}</td>
<td className="p-2"><details><summary className="cursor-pointer">{summary.documentIssues.length ? `${summary.documentIssues.length} belge kontrolü` : 'Kayıtlı belgelerde eksik bulunmadı'}</summary>
<ul className="space-y-2 mt-2">{summary.documentIssues.map((text,i)=><li key={i}><button onClick={()=>open(tender,'checks')} className="text-blue-800 underline text-left">{text}</button></li>)}</ul>
</details></td>
<td className="p-2"><button onClick={()=>open(tender,summary.next.target)} className="text-blue-800 underline text-left">{summary.next.text}</button>
<details className="mt-2"><summary className="cursor-pointer text-xs">Tüm eksikleri göster</summary><ul className="space-y-2 mt-2">{summary.issues.map((item,i)=><li key={i}><button onClick={()=>open(tender,item.target)} className="text-blue-800 underline text-left">{item.text}</button></li>)}</ul></details>
</td>
</tr>;
})}</tbody>
</table></div>}
</section>;
}
+2 -1
View File
@@ -42,6 +42,7 @@ export const TenderDetailModal = () => {
}
};
const [activeTab, setActiveTab] = useState('details'); // 'details', 'pdf_stamp', 'ai_assistant', 'checklist', 'kurum_tenders'
useEffect(()=>{setActiveTab(activeTenderDetail?.preparationTarget ? 'pdf_stamp' : 'details');},[activeTenderDetail?.id,activeTenderDetail?.preparationTarget]);
const noteOwner = authUser?.email || '';
const [noteDrafts, setNoteDrafts] = useState(() => ({ owner: noteOwner, drafts: {} }));
// Discard private drafts on account changes, before rendering another account.
@@ -434,7 +435,7 @@ export const TenderDetailModal = () => {
{/* Tab 2: Teklif dosyası hazırlığı */}
{activeTab === 'pdf_stamp' && (
<BidPackageBuilder key={tender.ikn || tender.id} tender={tender}
<BidPackageBuilder key={tender.ikn || tender.id} tender={tender} initialFocus={activeTenderDetail?.preparationTarget}
results={aiAnalysisResults?.tenderKey === (tender.ikn || tender.id) ? aiAnalysisResults : null} />
)}