fix: persist tender preparation and correct calendar export
This commit is contained in:
@@ -35,7 +35,7 @@ const bosFirma = () => Object.fromEntries(ALANLAR.map((a) => [a.k, '']));
|
||||
|
||||
export const BidPackageBuilder = ({ results, tender }) => {
|
||||
const { userProfile } = useApp();
|
||||
const [acik, setAcik] = useState(false);
|
||||
const [acik, setAcik] = useState(true);
|
||||
const [hata, setHata] = useState(null);
|
||||
|
||||
const [firma, setFirma] = useState(() => {
|
||||
@@ -49,11 +49,14 @@ export const BidPackageBuilder = ({ results, tender }) => {
|
||||
});
|
||||
|
||||
const [teklifTuru, setTeklifTuru] = useState('birim');
|
||||
const [gecerlilikGun, setGecerlilikGun] = useState('60');
|
||||
const kaynakGecerlilik = /(?:geçerlilik[^.]{0,100}?|ihale tarihinden itibaren\s*)(\d+)\s*(?:takvim\s*)?gün/i.exec(tender?.aciklama || '');
|
||||
const [gecerlilikGun, setGecerlilikGun] = useState(kaynakGecerlilik?.[1] || '');
|
||||
const [kisim, setKisim] = useState('');
|
||||
const [goturuBedel, setGoturuBedel] = useState('');
|
||||
|
||||
const kalemler = Array.isArray(results?.itemsAndSpecs) ? results.itemsAndSpecs : [];
|
||||
const kalemler = tender?.items?.length
|
||||
? tender.items.map(k => ({name:k.ad,quantity:`${k.miktar} ${k.birim || ''}`,keySpecs:[]}))
|
||||
: (Array.isArray(results?.itemsAndSpecs) ? results.itemsAndSpecs : []);
|
||||
const [fiyatlar, setFiyatlar] = useState({});
|
||||
|
||||
const alanYaz = (k, v) => {
|
||||
@@ -78,7 +81,13 @@ export const BidPackageBuilder = ({ results, tender }) => {
|
||||
const toplam = teklifTuru === 'goturu' ? (Number(goturuBedel) || 0) : kalemToplami;
|
||||
|
||||
const eksikler = ALANLAR.filter((a) => a.zorunlu && !String(firma[a.k] || '').trim());
|
||||
const hazir = eksikler.length === 0 && toplam > 0;
|
||||
const kalemEksik = teklifTuru === 'birim' && (!satirlar.length || satirlar.some(r => {
|
||||
const {miktar,birim} = miktarAyristir(r.quantity);
|
||||
return !r.name || !(miktar > 0) || !birim || !(Number(r.birimFiyat) > 0);
|
||||
}));
|
||||
const turCeliskisi = teklifTuru === 'goturu' && /birim fiyat/i.test(tender?.aciklama || '');
|
||||
const sureEksik = !(Number(gecerlilikGun) > 0) || (kaynakGecerlilik && Number(gecerlilikGun) < Number(kaynakGecerlilik[1]));
|
||||
const hazir = eksikler.length === 0 && toplam > 0 && !kalemEksik && !turCeliskisi && !sureEksik;
|
||||
|
||||
const formVerisi = () => ({
|
||||
...firma,
|
||||
@@ -94,6 +103,7 @@ export const BidPackageBuilder = ({ results, tender }) => {
|
||||
const uret = (tip) => {
|
||||
setHata(null);
|
||||
try {
|
||||
if (!hazir) throw new Error('Kalem, miktar, birim, fiyat, firma ve geçerlilik süresindeki eksikleri giderin.');
|
||||
const f = formVerisi();
|
||||
if (tip === 'mektup') belgeYazdir('Teklif Mektubu', teklifMektubuHtml(f, toplam));
|
||||
if (tip === 'cetvel') belgeYazdir('Birim Fiyat Teklif Cetveli', teklifCetveliHtml(f, satirlar, toplam));
|
||||
@@ -262,7 +272,7 @@ export const BidPackageBuilder = ({ results, tender }) => {
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-slate-500 bg-slate-50 border border-slate-200 rounded-lg p-3">
|
||||
Analizde kalem dökümü çıkmadı. Götürü bedel seçip toplam teklifinizi doğrudan girebilirsiniz.
|
||||
Kalem dökümü bulunamadı. Resmi teklif cetvelini İhale Dosyaları bölümünden yükleyin. Kalem eksikliği teklif türünü değiştirmez.
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -284,6 +294,11 @@ export const BidPackageBuilder = ({ results, tender }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(kalemEksik || turCeliskisi || sureEksik) && <p role="alert" className="text-xs bg-amber-50 p-3 text-amber-900">
|
||||
{kalemEksik && 'Seçilen her kalemde ad, miktar, birim ve fiyat gerekli. '}
|
||||
{turCeliskisi && 'İlan birim fiyat teklif belirtiyor; götürü bedel seçilemez. '}
|
||||
{sureEksik && 'Teklif geçerlilik süresini kaynak dokümana göre doğrulayın. '}
|
||||
</p>}
|
||||
{/* Belgeler */}
|
||||
<div className="space-y-2">
|
||||
<h5 className="font-extrabold text-xs text-slate-900">Hazırlanacak belgeler</h5>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { BidPackageBuilder } from './BidPackageBuilder';
|
||||
import { TenderDocuments } from './TenderDocuments';
|
||||
import { TeklifAsistani } from './TeklifAsistani';
|
||||
import { TeklifCetveliKalemleri } from './TeklifCetveliKalemleri';
|
||||
@@ -7,13 +8,13 @@ import {
|
||||
X, Star, Share2, Calendar, MapPin, Building2, Tag, FileText, CheckSquare,
|
||||
DollarSign, Calculator, Download, Sparkles, AlertTriangle, ShieldCheck,
|
||||
Send, Stamp, ArrowLeft, ExternalLink, Printer, PenLine } from 'lucide-react';
|
||||
import confetti from 'canvas-confetti';
|
||||
import { createTenderICS } from '../services/tenderCalendar';
|
||||
|
||||
export const TenderDetailModal = () => {
|
||||
const { activeTenderDetail, setActiveTenderDetail, toggleTrackTender, userProfile, setIsAiSpecAnalyzerOpen, gorevAc, gorevKontrol, aiAnalysisResults, tenders } = useApp();
|
||||
const { activeTenderDetail, setActiveTenderDetail, toggleTrackTender, userProfile, setIsAiSpecAnalyzerOpen, gorevAc, gorevKontrol, aiAnalysisResults, tenders, mutateTender, savingCount, saveError } = useApp();
|
||||
const [activeTab, setActiveTab] = useState('details'); // 'details', 'pdf_stamp', 'ai_assistant', 'checklist', 'kurum_tenders'
|
||||
const [noteText, setNoteText] = useState('');
|
||||
const [savedNotes, setSavedNotes] = useState([]);
|
||||
const [actionError, setActionError] = useState(null);
|
||||
|
||||
// Kur Farkı Calculator State
|
||||
const [bidAmount, setBidAmount] = useState(15000);
|
||||
@@ -28,30 +29,23 @@ export const TenderDetailModal = () => {
|
||||
// Havuzdaki güncel kayıt: takip/görev değişince düğme etiketleri de değişsin.
|
||||
const tender = (activeTenderDetail && tenders.find((t) => t.id === activeTenderDetail.id)) || activeTenderDetail;
|
||||
|
||||
const handleAddNote = (e) => {
|
||||
const savedNotes = tender.notes || [];
|
||||
const handleAddNote = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!noteText.trim()) return;
|
||||
setSavedNotes([...savedNotes, {
|
||||
const saved = await mutateTender(tender.id, {notes: [...savedNotes, {
|
||||
id: Date.now(),
|
||||
author: userProfile.name,
|
||||
text: noteText.trim(),
|
||||
date: 'Az önce'
|
||||
}]);
|
||||
setNoteText('');
|
||||
date: new Date().toLocaleString('tr-TR')
|
||||
}]});
|
||||
if (saved) setNoteText('');
|
||||
};
|
||||
|
||||
const handleExportICS = () => {
|
||||
confetti({ particleCount: 50 });
|
||||
const icsData = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//İhaleist//İhale Takvimi//TR
|
||||
BEGIN:VEVENT
|
||||
SUMMARY:İhale Son Teslim: ${tender.baslik}
|
||||
DESCRIPTION:İKN: ${tender.ikn} - ${tender.kurum}
|
||||
DTSTART:20260810T120000Z
|
||||
DTEND:20260810T130000Z
|
||||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
let icsData;
|
||||
try { icsData = createTenderICS(tender); setActionError(null); }
|
||||
catch (e) { setActionError(e.message); return; }
|
||||
|
||||
const blob = new Blob([icsData], { type: 'text/calendar;charset=utf-8' });
|
||||
const link = document.createElement('a');
|
||||
@@ -60,6 +54,7 @@ END:VCALENDAR`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
setTimeout(() => URL.revokeObjectURL(link.href), 1000);
|
||||
};
|
||||
|
||||
const handleStampMouseDown = () => setIsDraggingStamp(true);
|
||||
@@ -77,6 +72,8 @@ END:VCALENDAR`;
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto bg-slate-900/70 backdrop-blur-xs flex justify-center p-2 sm:p-4 animate-fade-in">
|
||||
<div className="bg-white w-full max-w-6xl rounded-2xl shadow-2xl border border-slate-200 overflow-hidden my-auto flex flex-col max-h-[92vh]">
|
||||
|
||||
{(actionError || saveError) && <p role="alert" className="p-3 bg-red-50 text-red-900">{actionError || saveError}</p>}
|
||||
{savingCount > 0 && <p role="status" className="p-2 bg-blue-50">Kaydediliyor…</p>}
|
||||
{/* Top Header Controls */}
|
||||
<div className="bg-[#1f2937] text-white px-6 py-3.5 flex items-center justify-between border-b border-slate-700 shrink-0">
|
||||
<button
|
||||
@@ -383,86 +380,8 @@ END:VCALENDAR`;
|
||||
|
||||
{/* Tab 2: PDF Birim Fiyat Cetveli & Kaşe Editörü */}
|
||||
{activeTab === 'pdf_stamp' && (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-amber-50 border border-amber-200 text-amber-900 p-4 rounded-xl text-xs flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
<Stamp className="w-5 h-5 text-amber-600" />
|
||||
<span>Teklif cetvelinizi doldurun ve Dijital Kaşenizi sürükleyerek belgenin altına yerleştirin.</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
import('../services/pdf').then(({ generateBidSheetPDF }) => {
|
||||
generateBidSheetPDF(tender, { firma: userProfile.company, stamp: true });
|
||||
});
|
||||
}}
|
||||
className="bg-amber-600 hover:bg-amber-500 text-white font-bold px-4 py-2 rounded-lg flex items-center gap-1.5 shadow-2xs"
|
||||
>
|
||||
<Download className="w-4 h-4" /> Kaşeli PDF İndir
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Interactive PDF Sheet Canvas Simulator */}
|
||||
<div
|
||||
onMouseMove={handleStampMouseMove}
|
||||
onMouseUp={handleStampMouseUp}
|
||||
className="bg-slate-200 p-6 rounded-2xl border border-slate-300 relative select-none min-h-[500px] flex justify-center"
|
||||
>
|
||||
{/* Paper sheet */}
|
||||
<div className="bg-white w-full max-w-2xl p-8 rounded-lg shadow-xl border border-slate-300 relative space-y-6 text-slate-800">
|
||||
<div className="text-center border-b-2 border-slate-800 pb-4 space-y-1">
|
||||
<p className="font-extrabold text-sm uppercase">{tender.kurum}</p>
|
||||
<p className="font-bold text-xs text-blue-900">BİRİM FİYAT TEKLİF CETVELİ — İKN: {tender.ikn}</p>
|
||||
</div>
|
||||
|
||||
<table className="w-full text-left text-xs border border-slate-400">
|
||||
<thead className="bg-slate-100 font-bold border-b border-slate-400">
|
||||
<tr>
|
||||
<th className="p-2 border-r border-slate-400">Sıra</th>
|
||||
<th className="p-2 border-r border-slate-400">Malzeme Adı</th>
|
||||
<th className="p-2 border-r border-slate-400">Miktar</th>
|
||||
<th className="p-2 border-r border-slate-400">Birim Fiyat (TL)</th>
|
||||
<th className="p-2">Toplam (TL)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-300">
|
||||
{tender.items?.map((item, idx) => (
|
||||
<tr key={idx}>
|
||||
<td className="p-2 border-r border-slate-400 font-bold text-center">{idx + 1}</td>
|
||||
<td className="p-2 border-r border-slate-400 font-semibold">{item.ad}</td>
|
||||
<td className="p-2 border-r border-slate-400">{item.miktar} {item.birim}</td>
|
||||
<td className="p-2 border-r border-slate-400 bg-yellow-50 font-bold text-blue-900">
|
||||
{item.birimFiyat || 150} TL
|
||||
</td>
|
||||
<td className="p-2 font-mono font-bold text-emerald-800">
|
||||
{((item.birimFiyat || 150) * parseFloat(item.miktar)).toLocaleString('tr-TR')} TL
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className="pt-12 text-right">
|
||||
<p className="font-bold text-xs text-slate-800">{userProfile.company}</p>
|
||||
<p className="text-[10px] text-slate-500">Yetkili İmza ve Kaşe Alanı</p>
|
||||
</div>
|
||||
|
||||
{/* Draggable Stamp Element */}
|
||||
<div
|
||||
onMouseDown={handleStampMouseDown}
|
||||
style={{ left: `${stampPos.x}px`, top: `${stampPos.y}px` }}
|
||||
className="absolute cursor-grab active:cursor-grabbing border-2 border-dashed border-red-500 bg-red-500/10 p-3 rounded-lg text-center transform -rotate-6 shadow-md transition-shadow hover:shadow-lg select-none"
|
||||
>
|
||||
<div className="w-32 h-16 border-2 border-red-700 rounded flex flex-col items-center justify-center p-1 text-red-700 font-black text-[9px] uppercase tracking-tighter">
|
||||
<span>{userProfile.company.slice(0, 25)}</span>
|
||||
<span className="text-[8px] text-red-600 font-bold mt-0.5">MÜDÜR İMZA & KAŞE</span>
|
||||
<span className="text-[7px] text-slate-500">2026-08-07 ONAYLI</span>
|
||||
</div>
|
||||
<span className="text-[9px] font-extrabold text-red-600 block mt-1">↕ Sürükleyip Bırakın</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BidPackageBuilder key={tender.ikn || tender.id} tender={tender}
|
||||
results={aiAnalysisResults?.tenderKey === (tender.ikn || tender.id) ? aiAnalysisResults : null} />
|
||||
)}
|
||||
|
||||
{/* Tab 3: AI Asistanı */}
|
||||
|
||||
@@ -20,6 +20,7 @@ export const ApiWebhooks = () => {
|
||||
|
||||
const yukle = () => {
|
||||
if (!authUser) return;
|
||||
setHata(null);
|
||||
listApiKeys().then(setDurum).catch((e) => setHata(e.message));
|
||||
};
|
||||
useEffect(yukle, [authUser]);
|
||||
@@ -53,6 +54,7 @@ export const ApiWebhooks = () => {
|
||||
|
||||
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-violet-950 to-indigo-900 text-white p-6 rounded-2xl shadow-lg border border-violet-800/40">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-2xl bg-violet-600 flex items-center justify-center shadow-md">
|
||||
@@ -111,7 +113,7 @@ export const ApiWebhooks = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!durum ? (
|
||||
{!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.anahtarlar.length === 0 ? (
|
||||
<p className="text-xs text-slate-400">Henüz anahtar yok.</p>
|
||||
|
||||
@@ -20,6 +20,7 @@ export const DocumentVault = () => {
|
||||
|
||||
const yukle = () => {
|
||||
if (!authUser) return;
|
||||
setHata(null);
|
||||
listDocuments().then(setDurum).catch((e) => setHata(e.message));
|
||||
};
|
||||
useEffect(yukle, [authUser]);
|
||||
@@ -106,6 +107,7 @@ export const DocumentVault = () => {
|
||||
|
||||
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">
|
||||
@@ -234,7 +236,7 @@ export const DocumentVault = () => {
|
||||
)}
|
||||
|
||||
{/* Liste */}
|
||||
{!durum ? (
|
||||
{!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">
|
||||
|
||||
Reference in New Issue
Block a user