fix: preserve newer tender note drafts after delayed saves
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 (
|
||||
<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]">
|
||||
@@ -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"
|
||||
/>
|
||||
<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
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user