Add health product portfolio and explicit tender item selection

This commit is contained in:
2026-09-06 13:34:06 +03:00
parent f7a04dadf5
commit 8e37b200e9
7 changed files with 77 additions and 2 deletions
+3 -1
View File
@@ -1,3 +1,4 @@
import {HealthPortfolioEditor} from './HealthPortfolioEditor';
import React,{useState,useEffect,useRef,useId} from 'react';
import {useApp} from '../context/AppContext';
import {getToken} from '../services/auth';
@@ -24,7 +25,7 @@ function ProfileEditor(){
const scan=e=>{e.preventDefault();void run('scan',async()=>{
controller.current=new AbortController();
const proposal=await scanCompanyWebsite(url,{signal:controller.current.signal});
if(alive.current){setCaps(proposal);setMessage('Tarama önerileri alındı. Alanları kontrol edip düzenleyin; henüz profilinize kaydedilmedi.');}
if(alive.current){setCaps(previous=>({...proposal,healthPortfolio:previous.healthPortfolio || []}));setMessage('Tarama önerileri alındı. Alanları kontrol edip düzenleyin; henüz profilinize kaydedilmedi.');}
});};
const save=()=>run('save',async()=>{
await updateCompanyCapabilities(caps,{website:url,threshold});
@@ -61,6 +62,7 @@ function ProfileEditor(){
<ul className="space-y-2">{(caps[key] || []).map((value,index)=><li key={`${value}:${index}`} className="flex justify-between gap-2 text-sm break-words"><span>{value}</span><button type="button" aria-label={`${label}: ${value} kaldır`} onClick={()=>edit(key,caps[key].filter((_,i)=>i!==index))} className="text-red-700 shrink-0">Kaldır</button></li>)}</ul>
</section>)}
</fieldset>
<HealthPortfolioEditor items={caps.healthPortfolio || []} disabled={!!busy} onChange={items=>edit('healthPortfolio',items)}/>
<label className="block text-sm">Uyum eşiği: %{threshold}<input aria-label="Uyum eşiği" type="range" min={30} max={95} step={5} disabled={!!busy} value={threshold} onChange={e=>{setThreshold(Number(e.target.value));setMessage('Kaydedilmemiş değişiklikler var.');}} className="block w-full"/></label>
<p className="text-xs text-slate-500">Uyum skoru terim eşleşmesi ve varsa anlamsal yakınlık girdisidir; başvuru yeterliliği değildir.</p>
<button type="button" onClick={save} disabled={!!busy} className="bg-blue-700 text-white px-4 py-2 rounded disabled:opacity-50">Profili kaydet</button>
+17
View File
@@ -0,0 +1,17 @@
import React from 'react';
import {portfolioFields} from '../services/healthPortfolio';
export function HealthPortfolioEditor({items,onChange,disabled}){
const patch=(id,change)=>onChange(items.map(item=>item.id===id?{...item,...change}:item));
return <fieldset disabled={disabled} className="border rounded-xl p-3 space-y-3">
<legend className="font-bold">Sağlık ürün ve hizmet portföyü</legend>
<p className="text-xs text-slate-600">Firma beyanıdır; resmi kayıt veya teknik uygunluk doğrulaması değildir. İhale hazırlığında seçilen ürünün bu sürümü taslakta tutulur.</p>
{items.map((item,index)=><section key={item.id} className="border rounded p-3 grid sm:grid-cols-2 gap-2">
<h3 className="font-bold text-sm sm:col-span-2">Portföy kaydı {index+1}</h3>
{portfolioFields.map(([key,label])=><label key={key} className="text-xs">{label}<input aria-label={`Portföy ${index+1} ${label}`} className="block w-full border rounded p-2" maxLength={1000} value={item[key] || ''} onChange={e=>patch(item.id,{[key]:e.target.value})}/></label>)}
<label className="text-xs">Kapsam<select aria-label={`Portföy ${index+1} Kapsam`} className="block w-full border rounded p-2" value={item.kind || 'consumable'} onChange={e=>patch(item.id,{kind:e.target.value})}><option value="consumable">Sarf / test ürünü</option><option value="device">Cihaz ve servis</option><option value="staffedService">Personelli hizmet</option></select></label>
<label className="text-xs">ÜTS beyanı<select aria-label={`Portföy ${index+1} ÜTS beyanı`} className="block w-full border rounded p-2" value={item.utsStatus || 'unknown'} onChange={e=>patch(item.id,{utsStatus:e.target.value})}><option value="unknown">Kontrol gerekli</option><option value="declared">Kayıt mevcut — firma beyanı</option><option value="notApplicable">Uygulanmıyor — açıklama gerekli</option></select></label>
<button type="button" className="text-red-700 text-xs" onClick={()=>onChange(items.filter(row=>row.id!==item.id))}>Portföy kaydı {index+1} kaldır</button>
</section>)}
<button type="button" className="border rounded p-2 text-sm" onClick={()=>onChange([...items,{id:crypto.randomUUID(),kind:'consumable',utsStatus:'unknown'}])}>Portföy kaydı ekle</button>
</fieldset>;
}
+14 -1
View File
@@ -1,3 +1,5 @@
import {useApp} from '../context/AppContext';
import {portfolioProductText,selectPortfolioProduct} from '../services/healthPortfolio';
import React, {useState} from 'react';
import {HEALTH_CHECKS, documentKey, documentRevision, documentScopeProblem} from '../services/preparationChecks';
const fieldClass = 'w-full border border-slate-300 rounded-lg p-2 text-xs bg-white';
@@ -6,6 +8,8 @@ const Status = ({value, onChange, optional = true}) => <select aria-label="Kontr
{optional && <option value="notApplicable">Uygulanmıyor — gerekçe gerekli</option>}
</select>;
export function PreparationChecks({rows, documents, manualDocuments, setManualDocuments, checks, setChecks, vault, refreshVault, downloadFile}) {
const {userProfile}=useApp();
const portfolio=userProfile.scrapedCapabilities?.healthPortfolio || [];
const eligibleFiles = vault.documents.filter(file => !documentScopeProblem(file,vault.tenderIkn));
const [name,setName] = useState('');
const [clause,setClause] = useState('');
@@ -56,7 +60,16 @@ export function PreparationChecks({rows, documents, manualDocuments, setManualDo
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">
<label className="block text-xs">Teklif edilen marka/model veya hizmet kapsamı<input className={fieldClass} value={c.product || ''} onChange={e=>updateProduct(row.priceKey,{product:e.target.value})}/></label>
<p className="text-xs text-slate-600">Ürün seçimini veya marka/model metnini değiştirirseniz teknik kontroller ve kaynak açıklamaları yeniden doldurulmalıdır.</p>
<label className="block text-xs">Firma portföyünden seç
<select className={fieldClass} value={c.portfolioId || ''} onChange={e=>updateProduct(row.priceKey,selectPortfolioProduct(portfolio.find(item=>item.id===e.target.value)))}>
<option value="">Elle kapsam girin</option>
{c.portfolioId&&!portfolio.some(item=>item.id===c.portfolioId)&&<option value={c.portfolioId}>Önceki portföy kaydı kaldırılmış</option>}
{portfolio.filter(item=>portfolioProductText(item)).map(item=><option key={item.id} value={item.id}>{portfolioProductText(item)}</option>)}
</select>
</label>
{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>
<label className="block text-xs">Ürün/hizmet uygunluğu<select className={fieldClass} value={c.status || 'pending'} onChange={e=>updateProduct(row.priceKey,{status:e.target.value})}>
<option value="pending">Kontrol gerekli</option><option value="suitable">Uygun</option><option value="unsuitable">Uygun değil</option>
+7
View File
@@ -0,0 +1,7 @@
import {HEALTH_CHECKS} from './preparationChecks.js';
export const portfolioFields=[['group','Ürün grubu'],['brand','Marka'],['model','Model / hizmet adı'],['utsReference','ÜTS kayıt numarası veya açıklaması'],['serviceCapacity','Kurulum / bakım / servis kapasitesi']];
export function portfolioProductText(item){return [item.group,item.brand,item.model].filter(Boolean).join(' · ');}
export function selectPortfolioProduct(item){
return {portfolioId:item?.id || null,portfolioSnapshot:item ? {...item} : null,product:item ? portfolioProductText(item) : '',source:'',status:'pending',
...Object.fromEntries(HEALTH_CHECKS.map(([key])=>[key,{status:'pending',evidence:''}]))};
}