diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index a19b3e8..b994026 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -154,3 +154,11 @@ Detayda açık kullanıcı işlemiyle incelendi/yeniden aç durumu sunucuya kayd ## Kullanıcı verisi yedek/geri yükleme aracı SQLite snapshot, referanslı belge kopyası, SHA-256 manifesti, bütünlük kontrolü ve yeni dizine geri yükleme eklendi. Çalışan yazıcıların durdurulması önkoşuldur; araç canlı üretim servisini kendisi durdurmaz. Bir test gerçek dosya ve hazırlık kaydının geri gelmesini, hedef üzerine yazmama ve bozuk yedek reddini doğrular. 46 test geçti. [Operasyon adımları ve kapsam sınırı](yedek-geri-yukleme.md). Üretim yedeği, tüm volume ve dağıtım geri dönüş provası hâlâ açık. + +### D01 — Not kaydı sırasında yeni düzenlemenin korunması + +- Önceki `await mutateTender` sonrası koşulsuz metin temizleme kaldırıldı. Başarılı yanıt yalnız gönderilen taslak nesnesi hâlâ güncelse o taslağı temizliyor; aynı metne dönülmüş olsa bile yeni düzenleme korunuyor. +- Kaydedilmemiş notlar bileşen oturumu içinde ihale bazında ayrılıyor. Hesap değişikliğinde temizleniyor; eski hesaptan/geçmiş oturumdan gelen yanıt başka taslağı temizleyemiyor. Bu geçici taslaklar sayfa yenilemesi için kalıcı kayıt değildir; Not Ekle ile sunucuya kaydedilen notlar mevcut kalıcı kayıt yolunu kullanıyor. +- Not alanına erişilebilir ad eklendi, kayıt beklerken/boş metinde gönderim düğmesi devre dışı bırakıldı. Kullanılmayan eski kaşe sürükleme durumu kaldırıldı. +- Doğrulama: 48/48 otomatik test ve üretim derlemesi geçti. İki yeni test gecikmiş yanıt, yeni düzenleme, aynı metne geri dönme, başka ihale ve hesap/oturum ayrımını kapsıyor. Gerçek tarayıcıda geciktirilmiş kayıt senaryosu bu adımda çalıştırılmadı. Mevcut paket boyutu uyarıları sürüyor. +- Tam plan, resmi beş dosya kabulü ve canlı yayın hâlâ tamamlanmış sayılmıyor. diff --git a/src/components/TenderDetailModal.jsx b/src/components/TenderDetailModal.jsx index c2e7523..28f29c9 100644 --- a/src/components/TenderDetailModal.jsx +++ b/src/components/TenderDetailModal.jsx @@ -8,12 +8,21 @@ 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 { acknowledgeNoteDraft, editNoteDraft } from '../services/noteDrafts'; +import { tenderStateKey } from '../services/tenderState'; import { createTenderICS } from '../services/tenderCalendar'; export const TenderDetailModal = () => { - const { activeTenderDetail, setActiveTenderDetail, toggleTrackTender, userProfile, setIsAiSpecAnalyzerOpen, gorevAc, gorevKontrol, aiAnalysisResults, tenders, mutateTender, savingCount, saveError } = useApp(); + const { activeTenderDetail, setActiveTenderDetail, toggleTrackTender, userProfile, setIsAiSpecAnalyzerOpen, gorevAc, gorevKontrol, aiAnalysisResults, tenders, mutateTender, savingCount, saveError, authUser } = useApp(); const [activeTab, setActiveTab] = useState('details'); // 'details', 'pdf_stamp', 'ai_assistant', 'checklist', 'kurum_tenders' - const [noteText, setNoteText] = useState(''); + const noteOwner = authUser?.email || ''; + const [noteDrafts, setNoteDrafts] = useState(() => ({ owner: noteOwner, drafts: {} })); + // Discard private drafts on account changes, before rendering another account. + if (noteDrafts.owner !== noteOwner) setNoteDrafts({ owner: noteOwner, drafts: {} }); + const noteKey = tenderStateKey(activeTenderDetail); + const noteDraft = noteDrafts.owner === noteOwner ? noteDrafts.drafts[noteKey] : undefined; + const noteText = noteDraft?.text || ''; + const setNoteText = text => setNoteDrafts(state => editNoteDraft(state, noteOwner, noteKey, text)); const [actionError, setActionError] = useState(null); // Kur Farkı Calculator State @@ -33,10 +42,6 @@ export const TenderDetailModal = () => { const usdRate = fx?.rates?.USD; const eurRate = fx?.rates?.EUR; - // Stamp Drag Position State - const [stampPos, setStampPos] = useState({ x: 220, y: 180 }); - const [isDraggingStamp, setIsDraggingStamp] = useState(false); - if (!activeTenderDetail) return null; // 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; @@ -51,7 +56,7 @@ export const TenderDetailModal = () => { text: noteText.trim(), date: new Date().toLocaleString('tr-TR') }]}); - if (saved) setNoteText(''); + if (saved) setNoteDrafts(state => acknowledgeNoteDraft(state, noteOwner, noteKey, noteDraft)); }; const handleExportICS = () => { @@ -69,17 +74,6 @@ export const TenderDetailModal = () => { setTimeout(() => URL.revokeObjectURL(link.href), 1000); }; - const handleStampMouseDown = () => setIsDraggingStamp(true); - const handleStampMouseMove = (e) => { - if (!isDraggingStamp) return; - const rect = e.currentTarget.getBoundingClientRect(); - setStampPos({ - x: Math.max(20, Math.min(rect.width - 150, e.clientX - rect.left - 60)), - y: Math.max(20, Math.min(rect.height - 80, e.clientY - rect.top - 30)) - }); - }; - const handleStampMouseUp = () => setIsDraggingStamp(false); - return (
@@ -316,10 +310,11 @@ export const TenderDetailModal = () => { type="text" value={noteText} onChange={(e) => setNoteText(e.target.value)} + aria-label="Ekip notu" placeholder="Ekip arkadaşlarınıza not yazın..." className="flex-1 bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-xs" /> - diff --git a/src/services/noteDrafts.js b/src/services/noteDrafts.js new file mode 100644 index 0000000..6fe3775 --- /dev/null +++ b/src/services/noteDrafts.js @@ -0,0 +1,12 @@ +// Draft objects identify the exact edit submitted; equal text can still be a new edit. +export function editNoteDraft(state, owner, key, text) { + if (state.owner !== owner) return state; + return { ...state, drafts: { ...state.drafts, [key]: { text } } }; +} + +export function acknowledgeNoteDraft(state, owner, key, submitted) { + if (state.owner !== owner || !submitted || state.drafts[key] !== submitted) return state; + const drafts = { ...state.drafts }; + delete drafts[key]; + return { ...state, drafts }; +} diff --git a/tests/note-drafts.test.mjs b/tests/note-drafts.test.mjs new file mode 100644 index 0000000..ab3e0e9 --- /dev/null +++ b/tests/note-drafts.test.mjs @@ -0,0 +1,25 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { editNoteDraft, acknowledgeNoteDraft } from '../src/services/noteDrafts.js'; + +test('late note acknowledgement preserves newer edits and other tender drafts', () => { + let state = editNoteDraft({ owner: 'a', drafts: {} }, 'a', 'giresun', 'İlk not'); + const submitted = state.drafts.giresun; + state = editNoteDraft(state, 'a', 'kayseri', 'Başka ihale'); + const acknowledged = acknowledgeNoteDraft(state, 'a', 'giresun', submitted); + assert.equal(acknowledged.drafts.giresun, undefined); + assert.equal(acknowledged.drafts.kayseri.text, 'Başka ihale'); + state = editNoteDraft(state, 'a', 'giresun', 'Yeni not'); + assert.equal(acknowledgeNoteDraft(state, 'a', 'giresun', submitted), state); + state = editNoteDraft(state, 'a', 'giresun', 'İlk not'); + assert.equal(acknowledgeNoteDraft(state, 'a', 'giresun', submitted), state); +}); + +test('old account acknowledgements cannot clear another session draft', () => { + const old = editNoteDraft({ owner: 'a', drafts: {} }, 'a', 'giresun', 'not'); + const other = editNoteDraft({ owner: 'b', drafts: {} }, 'b', 'giresun', 'not'); + assert.equal(acknowledgeNoteDraft(other, 'a', 'giresun', old.drafts.giresun), other); + assert.equal(editNoteDraft(other, 'a', 'giresun', 'late edit'), other); + const relogged = editNoteDraft({ owner: 'a', drafts: {} }, 'a', 'giresun', 'not'); + assert.equal(acknowledgeNoteDraft(relogged, 'a', 'giresun', old.drafts.giresun), relogged); +});