feat: validate preparation against owned document files
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { listDocuments, downloadDocument } from '../services/account';
|
||||
import { tenderDateKey } from '../services/tenderCalendar';
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { bidRowTotal, validateBidRows, roundMoney, parseBidPrice } from '../services/bidValidation';
|
||||
import { sourceRows, bidSourceSnapshot, restoreBidPrices } from '../services/bidSource';
|
||||
import { rowLot, selectBidLots } from '../services/bidLots';
|
||||
@@ -42,6 +44,15 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
|
||||
const { userProfile, mutateTender, savingCount, authUser, tenders } = useApp();
|
||||
const tender = {...selectedTender, ...tenders.find(t => t.id === selectedTender?.id)};
|
||||
const storedDraft = tender?.bidDraft || {};
|
||||
const [vault,setVault] = useState({loading:true,error:null,documents:[]});
|
||||
const [vaultRefresh,setVaultRefresh] = 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;};
|
||||
},[authUser?.email,vaultRefresh]);
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [acik, setAcik] = useState(true);
|
||||
const [hata, setHata] = useState(null);
|
||||
@@ -101,7 +112,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
|
||||
const kalemEksik = teklifTuru === 'birim' && validateBidRows(satirlar).length > 0;
|
||||
const turCeliskisi = teklifTuru === 'goturu' && /birim fiyat/i.test(tender?.aciklama || '');
|
||||
const sureEksik = !(Number(gecerlilikGun) > 0) || (kaynakGecerlilik && Number(gecerlilikGun) < Number(kaynakGecerlilik[1]));
|
||||
const kontrolEksikleri = preparationBlockers(lotSelection.rows, documents, checks);
|
||||
const kontrolEksikleri = preparationBlockers(lotSelection.rows, documents, checks, {...vault,deadlineDate:tenderDateKey(tender)});
|
||||
const hazir = eksikler.length === 0 && toplam > 0 && !kalemEksik && !turCeliskisi && !sureEksik && !kaynakKontrolGerekli && kontrolEksikleri.length === 0 && lotSelection.errors.length === 0;
|
||||
|
||||
const formVerisi = () => ({
|
||||
@@ -115,10 +126,15 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
|
||||
ihaleTarihi: tender?.ihaleTarihi || tender?.tarih || '',
|
||||
});
|
||||
|
||||
const uret = (tip) => {
|
||||
const uret = async (tip) => {
|
||||
setHata(null);
|
||||
try {
|
||||
if (!hazir) throw new Error('Kalem, miktar, birim, fiyat, firma ve geçerlilik süresindeki eksikleri giderin.');
|
||||
const currentFiles = await listDocuments();
|
||||
const currentVault = {loading:false,error:null,documents:currentFiles.belgeler || [],deadlineDate:tenderDateKey(tender)};
|
||||
setVault(currentVault);
|
||||
const fileErrors = preparationBlockers(lotSelection.rows, documents, checks, currentVault);
|
||||
if (fileErrors.length) throw new Error(fileErrors.join(' '));
|
||||
const f = formVerisi();
|
||||
if (tip === 'mektup') belgeYazdir('Teklif Mektubu', teklifMektubuHtml(f, toplam));
|
||||
if (tip === 'cetvel') belgeYazdir('Birim Fiyat Teklif Cetveli', teklifCetveliHtml(f, satirlar, toplam));
|
||||
@@ -332,7 +348,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
|
||||
)}
|
||||
|
||||
<PreparationChecks rows={lotSelection.rows} documents={documents} manualDocuments={manualDocuments}
|
||||
setManualDocuments={value => {setManualDocuments(value);setDirty(true);}} checks={checks} setChecks={setChecks}/>
|
||||
setManualDocuments={value => {setManualDocuments(value);setDirty(true);}} checks={checks} setChecks={setChecks} vault={vault} refreshVault={()=>setVaultRefresh(n=>n+1)} downloadFile={(file)=>downloadDocument(file.id,file.ad).catch(e=>setHata(e.message))}/>
|
||||
{kontrolEksikleri.length > 0 && <details className="bg-amber-50 border border-amber-300 rounded-xl p-3">
|
||||
<summary className="text-xs font-bold cursor-pointer">{kontrolEksikleri.length} belge/uygunluk kontrolü tamamlanmalı</summary>
|
||||
<ul className="text-xs list-disc pl-5 mt-2 space-y-1">{kontrolEksikleri.map((message,i)=><li key={i}>{message}</li>)}</ul>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, {useState} from 'react';
|
||||
import {HEALTH_CHECKS, documentKey} from '../services/preparationChecks';
|
||||
import {HEALTH_CHECKS, documentKey, documentRevision} from '../services/preparationChecks';
|
||||
const fieldClass = 'w-full border border-slate-300 rounded-lg p-2 text-xs bg-white';
|
||||
const Status = ({value, onChange, optional = true}) => <select aria-label="Kontrol durumu" className={fieldClass} value={value || 'pending'} onChange={e => onChange(e.target.value)}>
|
||||
<option value="pending">Kontrol gerekli</option><option value="verified">Doğrulandı</option>
|
||||
{optional && <option value="notApplicable">Uygulanmıyor — gerekçe gerekli</option>}
|
||||
</select>;
|
||||
export function PreparationChecks({rows, documents, manualDocuments, setManualDocuments, checks, setChecks}) {
|
||||
export function PreparationChecks({rows, documents, manualDocuments, setManualDocuments, checks, setChecks, vault, refreshVault, downloadFile}) {
|
||||
const [name,setName] = useState('');
|
||||
const [clause,setClause] = useState('');
|
||||
const updateDocument = (key, patch) => setChecks(old => ({...old,documents:{...old.documents,[key]:{...old.documents?.[key],...patch}}}));
|
||||
@@ -14,6 +14,10 @@ export function PreparationChecks({rows, documents, manualDocuments, setManualDo
|
||||
<h5 className="font-extrabold text-sm">Belge ve ürün uygunluğu</h5>
|
||||
<p className="text-xs text-slate-600">Her kontrol için belge adı/sürümü ve kaynak maddeyi yazın. ÜTS, numune ve servis koşulları her ihalede zorunlu değildir; uygulanmıyorsa gerekçesini belirtin. Bu kayıtlar kullanıcı kontrolüdür; resmi uygunluk onayı değildir.</p>
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs">Dosyaları Doküman Deposu'na yükleyin, ardından burada ilgili belge gereksinimine bağlayın.</p>
|
||||
<button type="button" onClick={refreshVault} disabled={vault.loading} className="text-xs font-bold text-blue-700">Belge deposunu yenile</button>
|
||||
{vault.loading && <p role="status" className="text-xs">Belge deposu yükleniyor…</p>}
|
||||
{vault.error && <p role="alert" className="text-xs text-red-700">Belge deposu yüklenemedi: {vault.error}</p>}
|
||||
{documents.map(d => {
|
||||
const key = documentKey(d), c = checks.documents?.[key] || {};
|
||||
return <fieldset key={key} className="border rounded-xl p-3 space-y-2">
|
||||
@@ -21,6 +25,17 @@ export function PreparationChecks({rows, documents, manualDocuments, setManualDo
|
||||
<p className="text-xs text-slate-600">{d.note}</p>
|
||||
{manualDocuments.some(item => documentKey(item) === key) && <button type="button" className="text-xs text-red-700"
|
||||
onClick={() => setManualDocuments(manualDocuments.filter(item => documentKey(item) !== key))}>Elle eklenen gereksinimi kaldır</button>}
|
||||
<label className="block text-xs">Bağlı dosya
|
||||
<select className={fieldClass} value={c.documentId || ''} disabled={vault.loading || Boolean(vault.error)} onChange={e=>{
|
||||
const file=vault.documents.find(f=>f.id===e.target.value);
|
||||
updateDocument(key,{documentId:file?.id || null,documentRevision:file ? documentRevision(file) : null,status:'pending'});
|
||||
}}>
|
||||
<option value="">Dosya seçin</option>
|
||||
{c.documentId && !vault.documents.some(f=>f.id===c.documentId) && <option value={c.documentId}>Önceki dosya bulunamadı</option>}
|
||||
{vault.documents.map(file=><option key={file.id} value={file.id}>{file.ad} · {file.son_kullanma || 'Süre bilgisi yok'}{file.erisilebilir === false ? ' · erişilemiyor' : ''}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
{vault.documents.some(f=>f.id===c.documentId) && <button type="button" className="text-xs text-blue-700" onClick={()=>downloadFile(vault.documents.find(f=>f.id===c.documentId))}>Bağlı dosyayı indir</button>}
|
||||
<Status value={c.status} optional={d.mandatory === false} onChange={status => updateDocument(key,{status})}/>
|
||||
<label className="block text-xs">Belge adı/sürümü, geçerlilik kontrolü ve kaynak madde
|
||||
<input className={fieldClass} value={c.evidence || ''} onChange={e => updateDocument(key,{evidence:e.target.value})}/>
|
||||
|
||||
@@ -5,12 +5,25 @@ export const HEALTH_CHECKS = [
|
||||
['delivery', 'Teslimat ve süre'],
|
||||
];
|
||||
export const documentKey = d => JSON.stringify([d.document, d.mandatory !== false, d.note || '']);
|
||||
export function preparationBlockers(rows, documents, checks = {}) {
|
||||
export const documentRevision = file => JSON.stringify([file.id,file.olusturma,file.boyut,file.son_kullanma || null]);
|
||||
export function documentLinkProblem(check, files, deadlineDate) {
|
||||
const file = files.find(f=>f.id===check?.documentId);
|
||||
if (!file || file.erisilebilir === false) return 'Bağlı dosya bulunamadı veya erişilemiyor.';
|
||||
if (check.documentRevision !== documentRevision(file)) return 'Belge bilgisi değişti; dosyayı yeniden seçip kontrol edin.';
|
||||
if (file.son_kullanma && ((!/^\d{4}-\d{2}-\d{2}$/.test(file.son_kullanma) || Number.isNaN(Date.parse(file.son_kullanma+'T00:00:00Z')) || new Date(file.son_kullanma+'T00:00:00Z').toISOString().slice(0,10) !== file.son_kullanma) ||
|
||||
!deadlineDate || file.son_kullanma < deadlineDate)) return 'Belge son teklif tarihinde geçerli değil veya tarih doğrulanamadı.';
|
||||
return null;
|
||||
}
|
||||
export function preparationBlockers(rows, documents, checks = {}, vault) {
|
||||
const errors = [];
|
||||
if (!documents.length) errors.push('Belge gereksinimleri belirlenmedi. Resmi dokümana göre belge ekleyin.');
|
||||
for (const d of documents) {
|
||||
const c = checks.documents?.[documentKey(d)] || {};
|
||||
if (d.mandatory === false && c.status === 'notApplicable' && c.evidence?.trim()) continue;
|
||||
if (vault) {
|
||||
const problem = vault.loading ? 'Belge deposu yükleniyor.' : vault.error ? 'Belge deposu doğrulanamadı.' : documentLinkProblem(c,vault.documents || [],vault.deadlineDate);
|
||||
if(problem) errors.push(`${d.document}: ${problem}`);
|
||||
}
|
||||
if (c.status !== 'verified' || !c.evidence?.trim()) errors.push(`${d.document}: belge ve kaynak kontrolü eksik.`);
|
||||
}
|
||||
for (const row of rows) {
|
||||
|
||||
Reference in New Issue
Block a user