fix: export filtered tender list as real XLSX
This commit is contained in:
@@ -2,7 +2,8 @@ import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react'
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { TenderCard } from './TenderCard';
|
||||
import { Download, SlidersHorizontal, Layers, Loader2 } from 'lucide-react';
|
||||
import confetti from 'canvas-confetti';
|
||||
import { createTenderWorkbook } from '../services/tenderWorkbook';
|
||||
import { getToken } from '../services/auth';
|
||||
import { useDeadlineClock } from '../hooks/useDeadlineClock';
|
||||
import { acikIhale, hizliFiltreGecer, GECMIS_FILTRELER, compareTenderDeadlines } from '../services/ihaleTarih';
|
||||
|
||||
@@ -107,29 +108,27 @@ export const TenderList = () => {
|
||||
return () => gozlemci.disconnect();
|
||||
}, [dahaVar, dahaGoster]);
|
||||
|
||||
const handleExportExcel = () => {
|
||||
confetti({ particleCount: 80, spread: 60, origin: { y: 0.6 } });
|
||||
|
||||
const headers = ['IKN', 'Baslik', 'Kurum', 'Sehir', 'Tarih', 'Alim Turu', 'Sektor', 'CPV'];
|
||||
const rows = filteredTenders.map(t => [
|
||||
t.ikn,
|
||||
`"${t.baslik.replace(/"/g, '""')}"`,
|
||||
`"${t.kurum.replace(/"/g, '""')}"`,
|
||||
t.sehir,
|
||||
t.tarih,
|
||||
t.alimTuru,
|
||||
t.sektor,
|
||||
`"${(t.cpvLabel || (t.cpv || []).join(', ') || '').replace(/"/g, '""')}"`
|
||||
]);
|
||||
|
||||
const csvContent = 'data:text/csv;charset=utf-8,\uFEFF' + [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
|
||||
const encodedUri = encodeURI(csvContent);
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', encodedUri);
|
||||
link.setAttribute('download', `ihaleist_ihale_listesi_${Date.now()}.csv`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
const [exporting,setExporting] = useState(false);
|
||||
const [exportError,setExportError] = useState(null);
|
||||
const exportPending = useRef(false);
|
||||
const mounted = useRef(true);
|
||||
useEffect(()=>{mounted.current=true;return()=>{mounted.current=false;};},[]);
|
||||
const handleExportExcel = async () => {
|
||||
if (exportPending.current) return;
|
||||
exportPending.current = true; setExporting(true); setExportError(null);
|
||||
const session = getToken();
|
||||
// Export the full filtered result at click time, including rows below the fold.
|
||||
const selected = filteredTenders.map(t=>({...t}));
|
||||
try {
|
||||
const bytes = await createTenderWorkbook(selected);
|
||||
if (!mounted.current || getToken() !== session) return;
|
||||
const url = URL.createObjectURL(new Blob([bytes], {type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}));
|
||||
const link = document.createElement('a');
|
||||
link.href = url; link.download = `ihaleist_ihale_listesi_${Date.now()}.xlsx`;
|
||||
document.body.appendChild(link); link.click(); link.remove();
|
||||
setTimeout(()=>URL.revokeObjectURL(url),1000);
|
||||
} catch (error) { if (mounted.current && getToken() === session) setExportError(error.message); }
|
||||
finally { exportPending.current=false; if (mounted.current) setExporting(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -171,15 +170,16 @@ export const TenderList = () => {
|
||||
</select>
|
||||
|
||||
<button
|
||||
onClick={handleExportExcel}
|
||||
onClick={handleExportExcel} disabled={exporting || !filteredTenders.length}
|
||||
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" />
|
||||
<span>Excel İndir</span>
|
||||
<span>{exporting ? 'Excel hazırlanıyor…' : 'Excel İndir'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{exportError && <p role="alert" className="text-sm text-red-700">{exportError}</p>}
|
||||
{/* Tender Cards List */}
|
||||
{gorunenTenders.length === 0 ? (
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-12 text-center shadow-xs">
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// User/source text stays a string cell, including leading '=' and embedded newlines.
|
||||
export async function createTenderWorkbook(tenders) {
|
||||
if (!Array.isArray(tenders) || !tenders.length) throw new Error('Dışa aktarılacak ihale bulunamadı.');
|
||||
const {default:ExcelJS} = await import('exceljs');
|
||||
const book = new ExcelJS.Workbook();
|
||||
book.creator = 'İhaleist';
|
||||
const sheet = book.addWorksheet('İhale Listesi');
|
||||
sheet.columns = [
|
||||
['İKN',20],['Başlık',65],['Kurum',45],['Şehir',20],['İhale tarihi',18],
|
||||
['İhale saati',15],['Alım türü',25],['Sektör',30],['CPV',35],['Kaynak',55],
|
||||
].map(([header,width])=>({header,width}));
|
||||
for (const t of tenders) {
|
||||
sheet.addRow([t.ikn,t.baslik,t.kurum,t.sehir,t.tarih,t.saat,t.alimTuru,t.sektor,
|
||||
t.cpvLabel || (Array.isArray(t.cpv) ? t.cpv.join(', ') : t.cpv),t.kaynakUrl].map(v=>String(v ?? '')));
|
||||
}
|
||||
sheet.views = [{state:'frozen',ySplit:1}];
|
||||
sheet.autoFilter = {from:'A1',to:`J${tenders.length+1}`};
|
||||
sheet.getRow(1).font = {bold:true};
|
||||
sheet.eachRow(row=>{row.alignment={vertical:'top',wrapText:true};});
|
||||
return book.xlsx.writeBuffer();
|
||||
}
|
||||
Reference in New Issue
Block a user