Restrict preparation document links to their tender scope
This commit is contained in:
@@ -491,3 +491,15 @@ Ortak havuz istemcisi HTTP hata/bozuk yanıtı boş liste gibi sunmaz. Panel hat
|
|||||||
Gerçek tarayıcı kabulü: localhost:5175 QA Giresun detayında “İhale kaydında aşağıda gösterilen 1 kalem var” mesajı ve “Malzeme / Kalem Listesi (1 Kalem)” tablosu birlikte doğrulandı. Kanıt: `qa-items-source-message.json`. Bu sentetik veriyle kaynak gösterimi testidir; resmi cetvel doğruluğu veya tüm kaynak uyuşmazlıklarının çözümü değildir. Ağ hatası/iptal/zaman aşımı ise otomatik testlerde sınandı.
|
Gerçek tarayıcı kabulü: localhost:5175 QA Giresun detayında “İhale kaydında aşağıda gösterilen 1 kalem var” mesajı ve “Malzeme / Kalem Listesi (1 Kalem)” tablosu birlikte doğrulandı. Kanıt: `qa-items-source-message.json`. Bu sentetik veriyle kaynak gösterimi testidir; resmi cetvel doğruluğu veya tüm kaynak uyuşmazlıklarının çözümü değildir. Ağ hatası/iptal/zaman aşımı ise otomatik testlerde sınandı.
|
||||||
|
|
||||||
Tüm 106 test ve üretim derlemesi geçti. Büyük paket uyarıları sürüyor. Planın tamamı ve canlı yayın açık.
|
Tüm 106 test ve üretim derlemesi geçti. Büyük paket uyarıları sürüyor. Planın tamamı ve canlı yayın açık.
|
||||||
|
|
||||||
|
### D01 / D07 — Hazırlık belgelerinde ihale kapsamı
|
||||||
|
|
||||||
|
Hazırlık belge seçicisi bütün depo kayıtlarını sunuyor; son kontrolde dosya erişimi, sürümü ve son kullanımı denetlenirken İKN/tür kapsamı kontrol edilmiyordu. Böylece aynı hesaba ait başka ihalenin dokümanı mevcut hazırlığa bağlanabiliyordu.
|
||||||
|
|
||||||
|
Yeni kapsam denetimi firma belgelerini yeniden kullanılabilir tutar; `ihale` türündeki dosyalar yalnız mevcut İKN ile eşleşince kullanılabilir. Türü bilinmeyen veya ihale bağlantısı olmayan kayıtlar açık hata verir. Seçici uygun dosyaları listeler; önceden bağlanmış uygunsuz dosyayı görünür bir “kullanılamıyor” seçeneğiyle korur, sessizce başka dosya seçmez. Genel hazır olma kontrolü ve çıktı öncesi güncel envanter kontrolü aynı İKN bağlamını kullanır.
|
||||||
|
|
||||||
|
Belge kontrol sürümüne tür ve İKN eklendi. Bu alanların değişmesi yeniden inceleme gerektirir; eski sürüm biçimiyle kaydedilmiş bağlantılar da güvenli biçimde yeniden seçim/kontrol ister. Bu, belgelerin gerçek yeterlik içeriğini otomatik doğrulamaz.
|
||||||
|
|
||||||
|
Testler: başka ihale belgesi reddi, aynı ihale kabulü, firma belgesinin yeniden kullanımı, tüm hazırlık kontrolüne kapsamın yansıması, tür/bağ değişiminde inceleme geçersizliği. Tarayıcıda yeni seçici kapsamı kabulü halen açık.
|
||||||
|
|
||||||
|
Sonuç: Tüm 108 test ve üretim derlemesi geçti. Büyük paket uyarıları sürüyor. Planın tümü bitmedi; canlı yayın yapılmadı.
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
|
|||||||
const kalemEksik = teklifTuru === 'birim' && validateBidRows(satirlar).length > 0;
|
const kalemEksik = teklifTuru === 'birim' && validateBidRows(satirlar).length > 0;
|
||||||
const turCeliskisi = !['birim','goturu'].includes(teklifTuru) || kaynakTeklifTuru.status === 'conflict' || (kaynakTeklifTuru.type && teklifTuru !== kaynakTeklifTuru.type);
|
const turCeliskisi = !['birim','goturu'].includes(teklifTuru) || kaynakTeklifTuru.status === 'conflict' || (kaynakTeklifTuru.type && teklifTuru !== kaynakTeklifTuru.type);
|
||||||
const sureEksik = !Number.isInteger(Number(gecerlilikGun)) || !(Number(gecerlilikGun) > 0) || kaynakGecerlilik.status === 'conflict' || (kaynakGecerlilik.days && Number(gecerlilikGun) < kaynakGecerlilik.days);
|
const sureEksik = !Number.isInteger(Number(gecerlilikGun)) || !(Number(gecerlilikGun) > 0) || kaynakGecerlilik.status === 'conflict' || (kaynakGecerlilik.days && Number(gecerlilikGun) < kaynakGecerlilik.days);
|
||||||
const kontrolEksikleri = preparationBlockers(lotSelection.rows, documents, checks, {...vault,deadlineDate:tenderDateKey(tender)});
|
const kontrolEksikleri = preparationBlockers(lotSelection.rows, documents, checks, {...vault,deadlineDate:tenderDateKey(tender),tenderIkn:tender.ikn});
|
||||||
const analizEksik = effectiveResults.sourceReview?.status === 'partial';
|
const analizEksik = effectiveResults.sourceReview?.status === 'partial';
|
||||||
const hazir = eksikler.length === 0 && toplam > 0 && !kalemEksik && !turCeliskisi && !sureEksik && !kaynakKontrolGerekli && kontrolEksikleri.length === 0 && lotSelection.errors.length === 0 && !analizEksik && !sourceOptions.needsSelection;
|
const hazir = eksikler.length === 0 && toplam > 0 && !kalemEksik && !turCeliskisi && !sureEksik && !kaynakKontrolGerekli && kontrolEksikleri.length === 0 && lotSelection.errors.length === 0 && !analizEksik && !sourceOptions.needsSelection;
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
|
|||||||
if (reviewedText) await validateDocumentTextReview(reviewedText,tender.ikn);
|
if (reviewedText) await validateDocumentTextReview(reviewedText,tender.ikn);
|
||||||
if (!mounted.current || session !== getToken() || submitted !== latestDraft.current)
|
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.');
|
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)};
|
const currentVault = {loading:false,error:null,documents:currentFiles.belgeler || [],deadlineDate:tenderDateKey(tender),tenderIkn:tender.ikn};
|
||||||
setVault(currentVault);
|
setVault(currentVault);
|
||||||
const fileErrors = preparationBlockers(lotSelection.rows, documents, checks, currentVault);
|
const fileErrors = preparationBlockers(lotSelection.rows, documents, checks, currentVault);
|
||||||
if (fileErrors.length) throw new Error(fileErrors.join(' '));
|
if (fileErrors.length) throw new Error(fileErrors.join(' '));
|
||||||
@@ -415,7 +415,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<PreparationChecks rows={lotSelection.rows} documents={documents} manualDocuments={manualDocuments}
|
<PreparationChecks rows={lotSelection.rows} documents={documents} manualDocuments={manualDocuments}
|
||||||
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))}/>
|
setManualDocuments={value => {setManualDocuments(value);setDirty(true);}} checks={checks} setChecks={setChecks} vault={{...vault,tenderIkn:tender.ikn}} 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">
|
{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>
|
<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>
|
<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,12 @@
|
|||||||
import React, {useState} from 'react';
|
import React, {useState} from 'react';
|
||||||
import {HEALTH_CHECKS, documentKey, documentRevision} from '../services/preparationChecks';
|
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';
|
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)}>
|
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>
|
<option value="pending">Kontrol gerekli</option><option value="verified">Doğrulandı</option>
|
||||||
{optional && <option value="notApplicable">Uygulanmıyor — gerekçe gerekli</option>}
|
{optional && <option value="notApplicable">Uygulanmıyor — gerekçe gerekli</option>}
|
||||||
</select>;
|
</select>;
|
||||||
export function PreparationChecks({rows, documents, manualDocuments, setManualDocuments, checks, setChecks, vault, refreshVault, downloadFile}) {
|
export function PreparationChecks({rows, documents, manualDocuments, setManualDocuments, checks, setChecks, vault, refreshVault, downloadFile}) {
|
||||||
|
const eligibleFiles = vault.documents.filter(file => !documentScopeProblem(file,vault.tenderIkn));
|
||||||
const [name,setName] = useState('');
|
const [name,setName] = useState('');
|
||||||
const [clause,setClause] = useState('');
|
const [clause,setClause] = useState('');
|
||||||
const updateDocument = (key, patch) => setChecks(old => ({...old,documents:{...old.documents,[key]:{...old.documents?.[key],...patch}}}));
|
const updateDocument = (key, patch) => setChecks(old => ({...old,documents:{...old.documents,[key]:{...old.documents?.[key],...patch}}}));
|
||||||
@@ -14,7 +15,7 @@ export function PreparationChecks({rows, documents, manualDocuments, setManualDo
|
|||||||
<h5 className="font-extrabold text-sm">Belge ve ürün uygunluğu</h5>
|
<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>
|
<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">
|
<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>
|
<p className="text-xs">Firma belgelerini veya bu İKN için yüklenmiş ihale dokümanlarını ilgili gereksinime bağlayın. Başka ihale dokümanları seçilemez.</p>
|
||||||
<button type="button" onClick={refreshVault} disabled={vault.loading} className="text-xs font-bold text-blue-700">Belge deposunu yenile</button>
|
<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.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>}
|
{vault.error && <p role="alert" className="text-xs text-red-700">Belge deposu yüklenemedi: {vault.error}</p>}
|
||||||
@@ -27,12 +28,12 @@ export function PreparationChecks({rows, documents, manualDocuments, setManualDo
|
|||||||
onClick={() => setManualDocuments(manualDocuments.filter(item => documentKey(item) !== key))}>Elle eklenen gereksinimi kaldır</button>}
|
onClick={() => setManualDocuments(manualDocuments.filter(item => documentKey(item) !== key))}>Elle eklenen gereksinimi kaldır</button>}
|
||||||
<label className="block text-xs">Bağlı dosya
|
<label className="block text-xs">Bağlı dosya
|
||||||
<select className={fieldClass} value={c.documentId || ''} disabled={vault.loading || Boolean(vault.error)} onChange={e=>{
|
<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);
|
const file=eligibleFiles.find(f=>f.id===e.target.value);
|
||||||
updateDocument(key,{documentId:file?.id || null,documentRevision:file ? documentRevision(file) : null,status:'pending'});
|
updateDocument(key,{documentId:file?.id || null,documentRevision:file ? documentRevision(file) : null,status:'pending'});
|
||||||
}}>
|
}}>
|
||||||
<option value="">Dosya seçin</option>
|
<option value="">Dosya seçin</option>
|
||||||
{c.documentId && !vault.documents.some(f=>f.id===c.documentId) && <option value={c.documentId}>Önceki dosya bulunamadı</option>}
|
{c.documentId && !eligibleFiles.some(f=>f.id===c.documentId) && <option value={c.documentId}>Önceki dosya bulunamadı veya bu ihale için kullanılamıyor</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>)}
|
{eligibleFiles.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>
|
</select>
|
||||||
</label>
|
</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>}
|
{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>}
|
||||||
|
|||||||
@@ -5,10 +5,19 @@ export const HEALTH_CHECKS = [
|
|||||||
['delivery', 'Teslimat ve süre'],
|
['delivery', 'Teslimat ve süre'],
|
||||||
];
|
];
|
||||||
export const documentKey = d => JSON.stringify([d.document, d.mandatory !== false, d.note || '']);
|
export const documentKey = d => JSON.stringify([d.document, d.mandatory !== false, d.note || '']);
|
||||||
export const documentRevision = file => JSON.stringify([file.id,file.olusturma,file.boyut,file.son_kullanma || null]);
|
export const documentRevision = file => JSON.stringify([file.id,file.olusturma,file.boyut,file.son_kullanma || null,file.tur || null,file.ikn || null]);
|
||||||
export function documentLinkProblem(check, files, deadlineDate) {
|
export function documentScopeProblem(file, tenderIkn) {
|
||||||
|
if (file.tur === 'firma') return null;
|
||||||
|
if (file.tur !== 'ihale') return 'Belge türü doğrulanamadı. Doküman Deposu kaydını kontrol edin.';
|
||||||
|
if (!tenderIkn || !file.ikn || String(file.ikn).trim() !== String(tenderIkn).trim())
|
||||||
|
return 'Bu ihale dokümanı mevcut ihaleye bağlı değil. Doğru İKN için yüklenmiş dosyayı seçin.';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
export function documentLinkProblem(check, files, deadlineDate, tenderIkn) {
|
||||||
const file = files.find(f=>f.id===check?.documentId);
|
const file = files.find(f=>f.id===check?.documentId);
|
||||||
if (!file || file.erisilebilir === false) return 'Bağlı dosya bulunamadı veya erişilemiyor.';
|
if (!file || file.erisilebilir === false) return 'Bağlı dosya bulunamadı veya erişilemiyor.';
|
||||||
|
const scopeProblem = documentScopeProblem(file,tenderIkn);
|
||||||
|
if (scopeProblem) return scopeProblem;
|
||||||
if (check.documentRevision !== documentRevision(file)) return 'Belge bilgisi değişti; dosyayı yeniden seçip kontrol edin.';
|
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) ||
|
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ı.';
|
!deadlineDate || file.son_kullanma < deadlineDate)) return 'Belge son teklif tarihinde geçerli değil veya tarih doğrulanamadı.';
|
||||||
@@ -21,7 +30,7 @@ export function preparationBlockers(rows, documents, checks = {}, vault) {
|
|||||||
const c = checks.documents?.[documentKey(d)] || {};
|
const c = checks.documents?.[documentKey(d)] || {};
|
||||||
if (d.mandatory === false && c.status === 'notApplicable' && c.evidence?.trim()) continue;
|
if (d.mandatory === false && c.status === 'notApplicable' && c.evidence?.trim()) continue;
|
||||||
if (vault) {
|
if (vault) {
|
||||||
const problem = vault.loading ? 'Belge deposu yükleniyor.' : vault.error ? 'Belge deposu doğrulanamadı.' : documentLinkProblem(c,vault.documents || [],vault.deadlineDate);
|
const problem = vault.loading ? 'Belge deposu yükleniyor.' : vault.error ? 'Belge deposu doğrulanamadı.' : documentLinkProblem(c,vault.documents || [],vault.deadlineDate,vault.tenderIkn);
|
||||||
if(problem) errors.push(`${d.document}: ${problem}`);
|
if(problem) errors.push(`${d.document}: ${problem}`);
|
||||||
}
|
}
|
||||||
if (c.status !== 'verified' || !c.evidence?.trim()) errors.push(`${d.document}: belge ve kaynak kontrolü eksik.`);
|
if (c.status !== 'verified' || !c.evidence?.trim()) errors.push(`${d.document}: belge ve kaynak kontrolü eksik.`);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ test('changed requirement and changed product row require new checks',()=>{
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('linked file must exist, match reviewed metadata and remain valid at tender deadline',()=>{
|
test('linked file must exist, match reviewed metadata and remain valid at tender deadline',()=>{
|
||||||
const file={id:'f1',olusturma:'2026-09-01',boyut:100,son_kullanma:'2026-09-10',erisilebilir:true};
|
const file={id:'f1',tur:'firma',olusturma:'2026-09-01',boyut:100,son_kullanma:'2026-09-10',erisilebilir:true};
|
||||||
const c={documentId:file.id,documentRevision:documentRevision(file)};
|
const c={documentId:file.id,documentRevision:documentRevision(file)};
|
||||||
assert.equal(documentLinkProblem(c,[file],'2026-09-10'),null);
|
assert.equal(documentLinkProblem(c,[file],'2026-09-10'),null);
|
||||||
assert.ok(documentLinkProblem(c,[],'2026-09-10'));
|
assert.ok(documentLinkProblem(c,[],'2026-09-10'));
|
||||||
@@ -29,3 +29,20 @@ test('linked file must exist, match reviewed metadata and remain valid at tender
|
|||||||
const invalid={...file,son_kullanma:'2026-02-31'};
|
const invalid={...file,son_kullanma:'2026-02-31'};
|
||||||
assert.ok(documentLinkProblem({...c,documentRevision:documentRevision(invalid)},[invalid],'2026-01-01'));
|
assert.ok(documentLinkProblem({...c,documentRevision:documentRevision(invalid)},[invalid],'2026-01-01'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('another tender document cannot satisfy preparation but company documents remain reusable',()=>{
|
||||||
|
const file={id:'doc1',tur:'ihale',ikn:'2026/111',olusturma:'2026-09-01',boyut:20};
|
||||||
|
const check={documentId:file.id,documentRevision:documentRevision(file)};
|
||||||
|
assert.match(documentLinkProblem(check,[file],'2026-09-10','2026/222'),/mevcut ihaleye bağlı değil/);
|
||||||
|
assert.equal(documentLinkProblem(check,[file],'2026-09-10','2026/111'),null);
|
||||||
|
const company={...file,tur:'firma',ikn:null};
|
||||||
|
assert.equal(documentLinkProblem({...check,documentRevision:documentRevision(company)},[company],'2026-09-10','2026/222'),null);
|
||||||
|
const c=complete();c.documents[documentKey(docs[0])]={...c.documents[documentKey(docs[0])],...check};
|
||||||
|
assert.ok(preparationBlockers(rows,docs,c,{documents:[file],deadlineDate:'2026-09-10',tenderIkn:'2026/222'}).some(e=>e.includes('mevcut ihaleye bağlı değil')));
|
||||||
|
});
|
||||||
|
test('classification or tender-link changes invalidate the previous review',()=>{
|
||||||
|
const file={id:'doc1',tur:'firma',ikn:null,olusturma:'2026-09-01',boyut:20};
|
||||||
|
const check={documentId:file.id,documentRevision:documentRevision(file)};
|
||||||
|
assert.match(documentLinkProblem(check,[{...file,tur:'ihale',ikn:'2026/111'}],'2026-09-10','2026/111'),/yeniden seçip/);
|
||||||
|
assert.match(documentLinkProblem(check,[{...file,tur:undefined}],'2026-09-10','2026/111'),/Belge türü/);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user