fix: invalidate preparation approval for changed portfolio products

This commit is contained in:
2026-09-06 13:52:40 +03:00
parent 52fdb5fe17
commit 36859188bf
7 changed files with 72 additions and 7 deletions
+10 -5
View File
@@ -4,7 +4,7 @@ import { preparationDeadlineProblem } from '../services/preparationDeadline';
import { bidValidityForForm, bidTypeForForm } from '../services/bidTerms';
import { createBidWorkbook, downloadBidWorkbook } from '../services/bidWorkbook';
import { getToken } from '../services/auth';
import { listDocuments, downloadDocument, validateDocumentTextReview } from '../services/account';
import { getAllUserData, listDocuments, downloadDocument, validateDocumentTextReview } from '../services/account';
import { tenderDateKey } from '../services/tenderCalendar';
import React, { useState, useMemo, useEffect, useRef, useId } from 'react';
import { bidRowTotal, validateBidRows, bidTotal, bidAmount } from '../services/bidValidation';
@@ -115,7 +115,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender, initialFocu
const kalemToplami = useMemo(() => bidTotal(satirlar), [satirlar]);
const latestDraft = useRef('');
latestDraft.current = JSON.stringify([firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,fiyatlar,sourceSnapshot,reviewedSource,checks,manualDocuments,partialBid,itemSource]);
latestDraft.current = JSON.stringify([firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,fiyatlar,sourceSnapshot,reviewedSource,checks,manualDocuments,partialBid,itemSource,userProfile.scrapedCapabilities?.healthPortfolio]);
const mounted = useRef(true);
const printWindow = useRef(null);
const [printing,setPrinting] = useState(false);
@@ -137,7 +137,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender, initialFocu
const [deadlineNow,setDeadlineNow] = useState(() => new Date());
useEffect(() => {const timer=setInterval(()=>setDeadlineNow(new Date()),30000);return ()=>clearInterval(timer);},[]);
const deadlineProblem = preparationDeadlineProblem(tender,deadlineNow);
const kontrolEksikleri = preparationBlockers(lotSelection.rows, documents, checks, {...vault,deadlineDate:tenderDateKey(tender),tenderIkn:tender.ikn});
const kontrolEksikleri = preparationBlockers(lotSelection.rows, documents, checks, {...vault,healthPortfolio:userProfile.scrapedCapabilities?.healthPortfolio || [],deadlineDate:tenderDateKey(tender),tenderIkn:tender.ikn});
const analizEksik = effectiveResults.sourceReview?.status === 'partial';
const hazir = !deadlineProblem && eksikler.length === 0 && toplam > 0 && !kalemEksik && !turCeliskisi && !sureEksik && !kaynakKontrolGerekli && kontrolEksikleri.length === 0 && lotSelection.errors.length === 0 && !analizEksik && !sourceOptions.needsSelection;
@@ -181,12 +181,14 @@ export const BidPackageBuilder = ({ results, tender: selectedTender, initialFocu
// Open during the click; async validation must not lose popup activation.
printWindow.current = belgeYazdir('Dosya kontrolü', '<p>Belgeler kontrol ediliyor…</p>');
setPrinting(true);
const currentFiles = await listDocuments();
const linkedPortfolio=lotSelection.rows.some(row=>checks.products?.[row.priceKey]?.portfolioId);
const readPortfolio=async()=>linkedPortfolio ? (await getAllUserData()).firma_profil?.scrapedCapabilities?.healthPortfolio || [] : [];
const [currentFiles,currentPortfolio] = await Promise.all([listDocuments(),readPortfolio()]);
const reviewedText = effectiveResults.sourceReview?.humanReview;
if (reviewedText) await validateDocumentTextReview(reviewedText,tender.ikn);
if (!mounted.current || session !== getToken() || submitted !== latestDraft.current)
throw new Error('Form, kaynak veya oturum değişti. Güncel bilgilerle yeniden çıktı oluşturun.');
const currentVault = {loading:false,error:null,documents:currentFiles.belgeler || [],deadlineDate:tenderDateKey(tender),tenderIkn:tender.ikn};
const currentVault = {healthPortfolio:currentPortfolio,loading:false,error:null,documents:currentFiles.belgeler || [],deadlineDate:tenderDateKey(tender),tenderIkn:tender.ikn};
setVault(currentVault);
const fileErrors = preparationBlockers(lotSelection.rows, documents, checks, currentVault);
if (fileErrors.length) throw new Error(fileErrors.join(' '));
@@ -196,6 +198,9 @@ export const BidPackageBuilder = ({ results, tender: selectedTender, initialFocu
if (tip === 'excel') {
const buffer = await createBidWorkbook(f,satirlar);
if (reviewedText) await validateDocumentTextReview(reviewedText,tender.ikn);
const finalPortfolio=await readPortfolio();
const finalErrors=preparationBlockers(lotSelection.rows,documents,checks,{...currentVault,healthPortfolio:finalPortfolio});
if(finalErrors.length)throw new Error(finalErrors.join(' '));
if (!mounted.current || session !== getToken() || submitted !== latestDraft.current)
throw new Error('Form veya oturum değişti; Excel çıktısını yeniden oluşturun.');
const finalDeadlineProblem=preparationDeadlineProblem(tender);
+7
View File
@@ -1,3 +1,4 @@
import {portfolioSelectionProblem} from '../services/portfolioRevision';
import {useApp} from '../context/AppContext';
import {portfolioProductText,selectPortfolioProduct} from '../services/healthPortfolio';
import React, {useState} from 'react';
@@ -57,6 +58,8 @@ export function PreparationChecks({rows, documents, manualDocuments, setManualDo
</div>
{rows.map(row => {
const c=checks.products?.[row.priceKey] || {};
const portfolioProblem=portfolioSelectionProblem(c,portfolio);
const currentProduct=portfolio.find(item=>item.id===c.portfolioId);
return <details key={row.priceKey} className="border rounded-xl p-3">
<summary className="cursor-pointer text-xs font-bold">{row.name} — ürün/hizmet ve yükümlülük kontrolü</summary>
<div className="space-y-2 mt-3">
@@ -68,6 +71,10 @@ export function PreparationChecks({rows, documents, manualDocuments, setManualDo
{portfolio.filter(item=>portfolioProductText(item)).map(item=><option key={item.id} value={item.id}>{portfolioProductText(item)}</option>)}
</select>
</label>
{portfolioProblem&&<div role="alert" className="text-xs text-red-700 border border-red-200 p-2 rounded">
<p>{portfolioProblem}</p>
{currentProduct&&<button type="button" className="underline mt-2" onClick={()=>updateProduct(row.priceKey,selectPortfolioProduct(currentProduct))}>Güncel ürün bilgilerini al ve kontrolleri sıfırla</button>}
</div>}
{c.portfolioSnapshot&&<p className="text-xs text-slate-600">Seçim anındaki firma beyanı: {c.portfolioSnapshot.kind==='device'?'Cihaz ve servis':c.portfolioSnapshot.kind==='staffedService'?'Personelli hizmet':'Sarf / test ürünü'} · ÜTS: {{declared:'Kayıt mevcut — firma beyanı',notApplicable:'Uygulanmıyor — firma beyanı',unknown:'Kontrol gerekli'}[c.portfolioSnapshot.utsStatus] || 'Kontrol gerekli'} · {c.portfolioSnapshot.utsReference || 'Kayıt açıklaması yok'} · Servis: {c.portfolioSnapshot.serviceCapacity || 'Belirtilmedi'}. İhale koşullarını aşağıda ayrıca doğrulayın.</p>}
<label className="block text-xs">Teklif edilen marka/model veya hizmet kapsamı<input className={fieldClass} value={c.product || ''} onChange={e=>updateProduct(row.priceKey,{...selectPortfolioProduct(null),product:e.target.value})}/></label>
<label className="block text-xs">Teknik doküman/madde ve uygunluk dayanağı<input className={fieldClass} value={c.source || ''} onChange={e=>updateProduct(row.priceKey,{source:e.target.value})}/></label>
+2 -2
View File
@@ -9,7 +9,7 @@ export function PreparationOverview({tenders,now}) {
return <Overview key={`${authUser?.email}:${getToken()}`} tenders={tenders} now={now}/>;
}
function Overview({tenders,now}) {
const {setActiveTenderDetail}=useApp();
const {setActiveTenderDetail,userProfile}=useApp();
const [vault,setVault]=useState({loading:true,error:null,documents:[]}),[refresh,setRefresh]=useState(0);
useEffect(()=>{
let cancelled=false;
@@ -29,7 +29,7 @@ function Overview({tenders,now}) {
<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);
const summary=preparationSummary(tender,{...vault,healthPortfolio:userProfile.scrapedCapabilities?.healthPortfolio || []},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>