Files
ihaleist/src/components/tools/DocumentVault.jsx
T

286 lines
14 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useRef } from 'react';
import { useApp } from '../../context/AppContext';
import { listDocuments, uploadDocument, deleteDocument, downloadDocument, formatBayt, updateDocumentMeta } from '../../services/account';
import { FolderKanban, Upload, Trash2, Download, Loader2, AlertTriangle, FileText, HardDrive, Layers, Package, CheckCircle2, XCircle, CalendarClock } from 'lucide-react';
const KATEGORILER = ['Yasal Belgeler', 'Kalite Belgeleri', 'Mali Belgeler', 'Resmi Kayıtlar', 'Teknik Belgeler', 'Diğer'];
/**
* Gerçek belge deposu — dosyalar sunucudaki kalıcı diske yazılır,
* boyut gerçek bayt cinsindendir ve plan kotası sunucu tarafında uygulanır.
* (Önceden dosya hiç yüklenmiyordu; boyut Math.random ile uyduruluyordu.)
*/
export const DocumentVault = () => {
const { authUser } = useApp();
const [durum, setDurum] = useState(null);
const [busy, setBusy] = useState(false);
const [hata, setHata] = useState(null);
const [kategori, setKategori] = useState(KATEGORILER[0]);
const dosyaRef = useRef(null);
const yukle = () => {
if (!authUser) return;
setHata(null);
listDocuments().then(setDurum).catch((e) => setHata(e.message));
};
useEffect(yukle, [authUser]);
const dosyaSecildi = async (e) => {
const f = e.target.files?.[0];
if (!f) return;
setBusy(true); setHata(null);
try {
setDurum(await uploadDocument(f, kategori));
} catch (err) {
setHata(err.message);
} finally {
setBusy(false);
if (dosyaRef.current) dosyaRef.current.value = '';
}
};
// Toplu ihale dokümanı: her dosyanın İKN'si METNİNDEN bulunur, kullanıcıya
// sorulmaz; kalemler otomatik havuza yazılır. docx/xlsx/zip sunucuda,
// PDF tarayıcıda (pdfjs) okunur. Onlarca şartnameyi tek seferde atmak için.
const topluRef = useRef(null);
const [toplu, setToplu] = useState([]); // [{ad, durum, ikn, kalem, hata}]
const topluSecildi = async (e) => {
const dosyalar = [...(e.target.files || [])];
if (!dosyalar.length) return;
setBusy(true); setHata(null);
setToplu(dosyalar.map((f) => ({ ad: f.name, durum: 'bekliyor' })));
const guncelle = (i, veri) => setToplu((o) => o.map((x, j) => (j === i ? { ...x, ...veri } : x)));
for (let i = 0; i < dosyalar.length; i++) {
const f = dosyalar[i];
guncelle(i, { durum: 'yukleniyor' });
try {
if (f.size > 10 * 1024 * 1024) throw new Error('10 MB sınırı');
const pdfGibi = /\.(pdf|txt|md)$/i.test(f.name);
let ikn = null;
let metin = null;
if (pdfGibi) {
const { extractText } = await import('../../services/pdfText');
const { iknCikar } = await import('../../services/kalem');
metin = (await extractText(f)).text;
ikn = iknCikar(metin);
}
const cevap = await uploadDocument(f, 'İhale Dokümanı', ikn, { otoIkn: true });
ikn = cevap?.ikn || ikn;
let kalem = cevap?.kalem?.sayi || null;
if (ikn && !kalem && metin) {
try {
const { kalemCikar } = await import('../../services/kalem');
kalem = (await kalemCikar({ ikn, metin, dosya: f.name })).sayi || null;
} catch { /* kalem çıkmazsa yükleme yine başarılı */ }
}
guncelle(i, { durum: 'tamam', ikn, kalem });
setDurum(cevap);
} catch (err) {
guncelle(i, { durum: 'hata', hata: err.message });
}
}
setBusy(false);
if (topluRef.current) topluRef.current.value = '';
};
// Şirket belgesi süre takibi: imza sirküleri, oda kayıt, borcu yoktur —
// hepsinin süresi dolar, kimse teklif hazırlanana kadar fark etmez.
const sureYaz = async (id, tarih) => {
setHata(null);
try { setDurum(await updateDocumentMeta(id, { sonKullanma: tarih || null })); }
catch (e) { setHata(e.message); }
};
const kalanGun = (b) => (b.son_kullanma ? Math.ceil((new Date(b.son_kullanma).getTime() - Date.now()) / 86400000) : null);
const suresiDolan = (durum?.belgeler || []).filter((b) => kalanGun(b) != null && kalanGun(b) < 0);
const yakinda = (durum?.belgeler || []).filter((b) => kalanGun(b) != null && kalanGun(b) >= 0 && kalanGun(b) <= 30);
const sil = async (id) => {
setBusy(true); setHata(null);
try { setDurum(await deleteDocument(id)); }
catch (e) { setHata(e.message); }
finally { setBusy(false); }
};
const k = durum?.kullanim;
const h = durum?.haklar;
const doluluk = h?.bayt ? Math.min(100, Math.round(((k?.bayt || 0) / h.bayt) * 100)) : 0;
return (
<div className="space-y-6 animate-fade-in">
{hata && <button onClick={yukle} className="px-4 py-2 border rounded-lg">Yeniden dene</button>}
<div className="bg-linear-to-r from-slate-900 via-teal-950 to-cyan-900 text-white p-6 rounded-2xl shadow-lg border border-teal-800/40">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-teal-600 flex items-center justify-center shadow-md">
<FolderKanban className="w-6 h-6" />
</div>
<div>
<h2 className="font-extrabold text-xl">Belge Kasası</h2>
<p className="text-xs text-teal-200 mt-0.5">
İhale dosyalarınıza her cihazdan erişin — belgeler sunucuda saklanır, tarayıcınızda değil.
</p>
</div>
</div>
</div>
{/* Kullanım */}
{durum && (
<div className="bg-white rounded-2xl border border-slate-200 shadow-xs p-5 space-y-2">
<div className="flex items-center justify-between text-xs">
<span className="font-bold text-slate-800 flex items-center gap-1.5">
<HardDrive className="w-3.5 h-3.5 text-teal-600" /> Kullanım
</span>
<span className="text-slate-600 font-semibold">
{k.adet}{h?.adet != null ? ` / ${h.adet}` : ''} belge · {formatBayt(k.bayt)}
{h?.bayt != null && ` / ${formatBayt(h.bayt)}`}
</span>
</div>
{h?.bayt != null && (
<div className="w-full bg-slate-100 rounded-full h-2 overflow-hidden">
<div className={`h-2 rounded-full transition-all ${doluluk > 90 ? 'bg-red-500' : doluluk > 70 ? 'bg-amber-500' : 'bg-teal-600'}`}
style={{ width: `${doluluk}%` }} />
</div>
)}
</div>
)}
{/* Yükleme */}
<div className="bg-white rounded-2xl border-2 border-dashed border-teal-300 p-6 text-center space-y-3">
<div className="w-12 h-12 rounded-2xl bg-teal-50 text-teal-600 flex items-center justify-center mx-auto">
<Upload className="w-6 h-6" />
</div>
<div>
<h3 className="font-bold text-sm text-slate-800">Belge Yükleyin</h3>
<p className="text-xs text-slate-500">İmza sirküleri, sertifikalar, vergi levhası… (en fazla 8 MB)</p>
</div>
<div className="flex justify-center gap-2 flex-wrap">
<select value={kategori} onChange={(e) => setKategori(e.target.value)}
className="bg-slate-50 border border-slate-200 rounded-lg px-2.5 py-2 text-xs">
{KATEGORILER.map((c) => <option key={c}>{c}</option>)}
</select>
<label className={`font-extrabold text-xs px-5 py-2.5 rounded-xl shadow-md flex items-center gap-2 ${
busy ? 'bg-slate-300 text-slate-500 cursor-not-allowed' : 'bg-teal-600 hover:bg-teal-500 text-white cursor-pointer'
}`}>
{busy ? <Loader2 className="w-4 h-4 animate-spin" /> : <Upload className="w-4 h-4" />}
{busy ? 'Yükleniyor…' : 'Dosya Seç'}
<input ref={dosyaRef} type="file" className="hidden" disabled={busy} onChange={dosyaSecildi} />
</label>
</div>
</div>
{/* Süre uyarısı */}
{(suresiDolan.length > 0 || yakinda.length > 0) && (
<div className={`rounded-2xl border-2 p-4 flex gap-3 ${suresiDolan.length ? 'bg-red-50 border-red-300' : 'bg-amber-50 border-amber-300'}`}>
<CalendarClock className={`w-5 h-5 shrink-0 mt-0.5 ${suresiDolan.length ? 'text-red-600' : 'text-amber-600'}`} />
<div className="text-xs leading-relaxed">
{suresiDolan.length > 0 && (
<p className="font-bold text-red-900">Süresi dolmuş: {suresiDolan.map((b) => b.ad).join(', ')}</p>
)}
{yakinda.length > 0 && (
<p className="font-semibold text-amber-900">30 gün içinde doluyor: {yakinda.map((b) => `${b.ad} (${kalanGun(b)} gün)`).join(', ')}</p>
)}
<p className="text-slate-600 mt-1">Teklif zarfına süresi dolmuş belge girerse teklif değerlendirme dışı kalır. Yenileyip yeni tarihi girin.</p>
</div>
</div>
)}
{/* Toplu ihale dokümanı yükleme */}
<div className="bg-linear-to-r from-slate-900 via-indigo-950 to-slate-900 rounded-2xl p-5 space-y-3">
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-xl bg-amber-400 text-slate-950 flex items-center justify-center shrink-0">
<Layers className="w-5 h-5" />
</div>
<div className="min-w-0">
<h3 className="font-black text-sm text-white">İhale Dokümanlarını Toplu Yükle</h3>
<p className="text-[11px] text-slate-300 leading-relaxed">
EKAP'tan indirdiğiniz şartname/doküman dosyalarını (PDF, DOCX, XLSX, ZIP) hepsini birden seçin.
Her dosyanın <strong className="text-white">İKN'si metninden bulunur</strong>, ihaleye bağlanır ve
teklif cetveli kalemleri otomatik çıkarılır — o ihale herkesin kartında kalemleriyle görünür.
</p>
</div>
</div>
<label className={`inline-flex font-black text-xs px-5 py-2.5 rounded-xl items-center gap-2 ${
busy ? 'bg-slate-600 text-slate-300 cursor-not-allowed' : 'bg-amber-400 hover:bg-amber-300 text-slate-950 cursor-pointer'
}`}>
{busy ? <Loader2 className="w-4 h-4 animate-spin" /> : <Upload className="w-4 h-4" />}
{busy ? 'İşleniyor…' : 'Dosyaları Seç (çoklu)'}
<input ref={topluRef} type="file" multiple accept=".pdf,.docx,.xlsx,.zip,.txt" className="hidden" disabled={busy} onChange={topluSecildi} />
</label>
{toplu.length > 0 && (
<ul className="space-y-1">
{toplu.map((t, i) => (
<li key={i} className="bg-black/30 rounded-lg px-3 py-2 flex items-center gap-2 text-[11px]">
{t.durum === 'tamam' ? <CheckCircle2 className="w-3.5 h-3.5 text-emerald-400 shrink-0" />
: t.durum === 'hata' ? <XCircle className="w-3.5 h-3.5 text-red-400 shrink-0" />
: <Loader2 className={`w-3.5 h-3.5 text-slate-400 shrink-0 ${t.durum === 'yukleniyor' ? 'animate-spin' : ''}`} />}
<span className="text-slate-200 truncate flex-1">{t.ad}</span>
{t.durum === 'tamam' && (
<span className="shrink-0 flex items-center gap-2">
{t.ikn
? <span className="font-mono font-bold text-amber-300">{t.ikn}</span>
: <span className="text-amber-200/80">İKN bulunamadı — ihaleye bağlanmadı</span>}
{t.kalem ? <span className="text-emerald-300 font-bold flex items-center gap-1"><Package className="w-3 h-3" />{t.kalem} kalem</span> : null}
</span>
)}
{t.durum === 'hata' && <span className="text-red-300 shrink-0">{t.hata}</span>}
</li>
))}
</ul>
)}
</div>
{hata && (
<div className="bg-red-50 border border-red-200 rounded-xl p-4 flex gap-3">
<AlertTriangle className="w-4 h-4 text-red-600 shrink-0 mt-0.5" />
<p className="text-xs text-red-800 leading-relaxed">{hata}</p>
</div>
)}
{/* Liste */}
{!durum && !hata && !hata ? (
<div className="flex items-center gap-2 text-xs text-slate-400"><Loader2 className="w-3.5 h-3.5 animate-spin" /> yükleniyor…</div>
) : durum.belgeler.length === 0 ? (
<div className="bg-white rounded-2xl border border-dashed border-slate-300 p-10 text-center">
<FileText className="w-8 h-8 text-slate-300 mx-auto mb-3" />
<p className="text-sm font-bold text-slate-500">Henüz belge yüklemediniz</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{durum.belgeler.map((d) => (
<div key={d.id} className="bg-white rounded-xl border border-slate-200 p-4 shadow-xs flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-xs font-bold text-slate-900 truncate">{d.ad}</p>
<p className="text-[11px] text-slate-500 mt-0.5">
{d.kategori || 'Diğer'} · <strong>{formatBayt(d.boyut)}</strong>
</p>
<p className="text-[10px] text-slate-400 mt-0.5">
{new Date(d.olusturma).toLocaleString('tr-TR')}
</p>
<label className="flex items-center gap-1.5 mt-1.5 text-[10px] text-slate-500">
<CalendarClock className={`w-3 h-3 ${kalanGun(d) != null && kalanGun(d) < 0 ? 'text-red-600' : kalanGun(d) != null && kalanGun(d) <= 30 ? 'text-amber-600' : 'text-slate-400'}`} />
<span>Son kullanma</span>
<input type="date" value={d.son_kullanma || ''} onChange={(e) => sureYaz(d.id, e.target.value)}
className="border border-slate-200 rounded px-1 py-0.5 text-[10px]" />
{kalanGun(d) != null && (
<span className={`font-bold ${kalanGun(d) < 0 ? 'text-red-700' : kalanGun(d) <= 30 ? 'text-amber-700' : 'text-emerald-700'}`}>
{kalanGun(d) < 0 ? `${-kalanGun(d)} gün önce doldu` : `${kalanGun(d)} gün kaldı`}
</span>
)}
</label>
</div>
<div className="flex items-center gap-1 shrink-0">
<button onClick={() => downloadDocument(d.id, d.ad).catch((e) => setHata(e.message))}
className="text-slate-400 hover:text-teal-600 p-1" title="İndir">
<Download className="w-4 h-4" />
</button>
<button onClick={() => sil(d.id)} disabled={busy} className="text-slate-400 hover:text-red-500 p-1" title="Sil">
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
))}
</div>
)}
</div>
);
};