fix: provide visible prepared Excel download link

This commit is contained in:
2026-09-06 09:28:58 +03:00
parent 90fc789a7a
commit 5e9f17969c
3 changed files with 22 additions and 6 deletions
@@ -0,0 +1,2 @@
09:28:30 itibarıyla 5 ihale için Excel hazır.Excel dosyasını indir — ihaleist_ihale_listesi_1788676110092.xlsx
İndirme bağlantısı doğrudan tıklandı. Diskte dosya doğrulaması yapılmadı.
+7
View File
@@ -272,3 +272,10 @@ SQLite snapshot, referanslı belge kopyası, SHA-256 manifesti, bütünlük kont
- Kanıtlar: `evidence/qa-integrated-search.txt`, `evidence/qa-integrated-form.txt` (yalnız yapay test firma verisi). - Kanıtlar: `evidence/qa-integrated-search.txt`, `evidence/qa-integrated-form.txt` (yalnız yapay test firma verisi).
- Excel düğmesi sonrası arayüz hata göstermedi; ancak download olayı 3 saniye ve yeniden denemede 15 saniye içinde gözlenemedi. İndirme GEÇTİ sayılmadı; tarayıcı olay izlemesi mi uygulama indirme yolu mu ayrıştırılmalı. Önceki dosya üretme/yeniden okuma testi bu uçtan uca açığı kapatmaz. - Excel düğmesi sonrası arayüz hata göstermedi; ancak download olayı 3 saniye ve yeniden denemede 15 saniye içinde gözlenemedi. İndirme GEÇTİ sayılmadı; tarayıcı olay izlemesi mi uygulama indirme yolu mu ayrıştırılmalı. Önceki dosya üretme/yeniden okuma testi bu uçtan uca açığı kapatmaz.
- Bu kanıt resmi beş ihale dosyası kabulü değildir; sentetik yerel regresyondur. Gerçek mobil viewport, gerçek belge ve resmi gönderim doğrulaması açık. - Bu kanıt resmi beş ihale dosyası kabulü değildir; sentetik yerel regresyondur. Gerçek mobil viewport, gerçek belge ve resmi gönderim doğrulaması açık.
### D03 / D09 — Görünür Excel indirme bağlantısı
- Liste dışa aktarımı artık “Excel Hazırla” → dosya adı/kayıt sayısı/hazırlanma saati → doğrudan indirme bağlantısı akışını kullanıyor. Asenkron üretim sonrası görünmez programatik tıklamaya bağımlılık kaldırıldı; hazır dosya tekrar indirilebilir.
- Oluşan blob bağlantısı yeni dosya hazırlanırken veya ekran kapanırken temizleniyor. Oturum değişince eski hazır dosya bağlantısı gösterilmiyor. Çıktı tıklama anındaki sonuç kümesini temsil ediyor; yeni filtre için yeniden hazırlama gerekiyor.
- Tarayıcıda beş sentetik ihale için dosya hazır durumu ve `.xlsx` bağlantısı görüldü, doğrudan bağlantı tıklandı. Kanıt: `evidence/qa-excel-ready-link.txt`. Üretim derlemesi geçti; dosya içeriği için önceki XLSX roundtrip regresyonu geçerlidir.
- İndirme geçmişini kontrol etmek üzere chrome://downloads erişimi tarayıcı URL güvenlik politikası tarafından engellendi; başka yoldan bu geçmişe erişilmedi. Diskte nihai dosya ve masaüstü Excel açılışı doğrulanmış sayılmıyor. Önceki indirme olayı zaman aşımının kesin kök nedeni henüz belirlenmedi.
+13 -6
View File
@@ -101,12 +101,16 @@ export const TenderList = () => {
const [exporting,setExporting] = useState(false); const [exporting,setExporting] = useState(false);
const [exportError,setExportError] = useState(null); const [exportError,setExportError] = useState(null);
const [preparedExport,setPreparedExport] = useState(null);
const preparedUrl = useRef(null);
const exportPending = useRef(false); const exportPending = useRef(false);
const mounted = useRef(true); const mounted = useRef(true);
useEffect(()=>{mounted.current=true;return()=>{mounted.current=false;};},[]); useEffect(()=>{mounted.current=true;return()=>{mounted.current=false;if(preparedUrl.current)URL.revokeObjectURL(preparedUrl.current);};},[]);
const handleExportExcel = async () => { const handleExportExcel = async () => {
if (exportPending.current) return; if (exportPending.current) return;
exportPending.current = true; setExporting(true); setExportError(null); exportPending.current = true; setExporting(true); setExportError(null);
if(preparedUrl.current) URL.revokeObjectURL(preparedUrl.current);
preparedUrl.current=null;setPreparedExport(null);
const session = getToken(); const session = getToken();
// Export the full filtered result at click time, including rows below the fold. // Export the full filtered result at click time, including rows below the fold.
const selected = filteredTenders.map(t=>({...t})); const selected = filteredTenders.map(t=>({...t}));
@@ -114,10 +118,9 @@ export const TenderList = () => {
const bytes = await createTenderWorkbook(selected); const bytes = await createTenderWorkbook(selected);
if (!mounted.current || getToken() !== session) return; if (!mounted.current || getToken() !== session) return;
const url = URL.createObjectURL(new Blob([bytes], {type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'})); const url = URL.createObjectURL(new Blob([bytes], {type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}));
const link = document.createElement('a'); preparedUrl.current=url;
link.href = url; link.download = `ihaleist_ihale_listesi_${Date.now()}.xlsx`; setPreparedExport({url,name:`ihaleist_ihale_listesi_${Date.now()}.xlsx`,count:selected.length,
document.body.appendChild(link); link.click(); link.remove(); query:searchQuery.trim(),session,createdAt:new Date().toLocaleTimeString('tr-TR')});
setTimeout(()=>URL.revokeObjectURL(url),1000);
} catch (error) { if (mounted.current && getToken() === session) setExportError(error.message); } } catch (error) { if (mounted.current && getToken() === session) setExportError(error.message); }
finally { exportPending.current=false; if (mounted.current) setExporting(false); } finally { exportPending.current=false; if (mounted.current) setExporting(false); }
}; };
@@ -165,12 +168,16 @@ export const TenderList = () => {
className="bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs px-3.5 py-2 rounded-lg flex items-center gap-1.5 transition-colors shadow-xs shrink-0" className="bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs px-3.5 py-2 rounded-lg flex items-center gap-1.5 transition-colors shadow-xs shrink-0"
> >
<Download className="w-4 h-4" /> <Download className="w-4 h-4" />
<span>{exporting ? 'Excel hazırlanıyor…' : 'Excel İndir'}</span> <span>{exporting ? 'Excel hazırlanıyor…' : 'Excel Hazırla'}</span>
</button> </button>
</div> </div>
</div> </div>
{sortBy === 'newest' && <p className="text-xs text-slate-500">Yayın tarihi bilinmeyen kayıtlar sonda gösterilir. Aynı gün içinde saati bilinen kayıtlar önce gelir.</p>} {sortBy === 'newest' && <p className="text-xs text-slate-500">Yayın tarihi bilinmeyen kayıtlar sonda gösterilir. Aynı gün içinde saati bilinen kayıtlar önce gelir.</p>}
{preparedExport && preparedExport.session === getToken() && <div role="status" className="rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-xs space-y-1">
<p>{preparedExport.createdAt} itibarıyla {preparedExport.count} ihale için Excel hazır{preparedExport.query ? ` — arama: ${preparedExport.query}` : ''}.</p>
<a href={preparedExport.url} download={preparedExport.name} className="font-bold text-emerald-800 underline">Excel dosyasını indir — {preparedExport.name}</a>
</div>}
{exportError && <p role="alert" className="text-sm text-red-700">{exportError}</p>} {exportError && <p role="alert" className="text-sm text-red-700">{exportError}</p>}
{/* Tender Cards List */} {/* Tender Cards List */}
{gorunenTenders.length === 0 ? ( {gorunenTenders.length === 0 ? (