fix: preserve newer tender note drafts after delayed saves

This commit is contained in:
2026-09-06 09:07:23 +03:00
parent d4d9d17a9f
commit 7ad5fb4f50
4 changed files with 59 additions and 19 deletions
+8
View File
@@ -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ı ## 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. 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.
+14 -19
View File
@@ -8,12 +8,21 @@ import {
X, Star, Share2, Calendar, MapPin, Building2, Tag, FileText, CheckSquare, X, Star, Share2, Calendar, MapPin, Building2, Tag, FileText, CheckSquare,
DollarSign, Calculator, Download, Sparkles, AlertTriangle, ShieldCheck, DollarSign, Calculator, Download, Sparkles, AlertTriangle, ShieldCheck,
Send, Stamp, ArrowLeft, ExternalLink, Printer, PenLine } from 'lucide-react'; 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'; import { createTenderICS } from '../services/tenderCalendar';
export const TenderDetailModal = () => { 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 [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); const [actionError, setActionError] = useState(null);
// Kur Farkı Calculator State // Kur Farkı Calculator State
@@ -33,10 +42,6 @@ export const TenderDetailModal = () => {
const usdRate = fx?.rates?.USD; const usdRate = fx?.rates?.USD;
const eurRate = fx?.rates?.EUR; 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; if (!activeTenderDetail) return null;
// Havuzdaki güncel kayıt: takip/görev değişince düğme etiketleri de değişsin. // 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 tender = (activeTenderDetail && tenders.find((t) => t.id === activeTenderDetail.id)) || activeTenderDetail;
@@ -51,7 +56,7 @@ export const TenderDetailModal = () => {
text: noteText.trim(), text: noteText.trim(),
date: new Date().toLocaleString('tr-TR') date: new Date().toLocaleString('tr-TR')
}]}); }]});
if (saved) setNoteText(''); if (saved) setNoteDrafts(state => acknowledgeNoteDraft(state, noteOwner, noteKey, noteDraft));
}; };
const handleExportICS = () => { const handleExportICS = () => {
@@ -69,17 +74,6 @@ export const TenderDetailModal = () => {
setTimeout(() => URL.revokeObjectURL(link.href), 1000); 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 ( return (
<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="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]"> <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]">
@@ -316,10 +310,11 @@ export const TenderDetailModal = () => {
type="text" type="text"
value={noteText} value={noteText}
onChange={(e) => setNoteText(e.target.value)} onChange={(e) => setNoteText(e.target.value)}
aria-label="Ekip notu"
placeholder="Ekip arkadaşlarınıza not yazın..." 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" className="flex-1 bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-xs"
/> />
<button type="submit" className="bg-blue-600 hover:bg-blue-500 text-white font-bold px-4 py-2 rounded-lg text-xs flex items-center gap-1"> <button type="submit" disabled={savingCount > 0 || !noteText.trim()} className="disabled:opacity-50 bg-blue-600 hover:bg-blue-500 text-white font-bold px-4 py-2 rounded-lg text-xs flex items-center gap-1">
<Send className="w-3.5 h-3.5" /> Not Ekle <Send className="w-3.5 h-3.5" /> Not Ekle
</button> </button>
</form> </form>
+12
View File
@@ -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 };
}
+25
View File
@@ -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);
});