fix: persist tender preparation and correct calendar export

This commit is contained in:
2026-09-06 08:30:32 +03:00
parent 909ebae7c2
commit c91631d99f
19 changed files with 569 additions and 182 deletions
+77 -49
View File
@@ -5,6 +5,8 @@ import { getToken } from '../services/auth';
import { fetchLiveTenders, fetchTenderDetail, mergeUserState } from '../services/tenderApi';
import { restoreSession, clearSession, getStoredUser } from '../services/auth';
import { getAllUserData, setUserData, getPlan, bildirimleriGetir, bildirimOkundu, listDocuments } from '../services/account';
import { applyTenderStates, tenderStateKey, tenderSnapshot } from '../services/tenderState';
import { getTenderStates, saveTenderState } from '../services/account';
import { SECTORS_LIST, INSTITUTION_TYPES_LIST } from '../data/constants';
const AppContext = createContext();
@@ -42,7 +44,7 @@ export const AppProvider = ({ children }) => {
try {
const saved = localStorage.getItem(TENDER_CACHE_KEY);
const parsed = saved ? JSON.parse(saved) : null;
return Array.isArray(parsed) ? parsed : [];
return Array.isArray(parsed) ? parsed.map(t => ({...t,takipte:false,gorev:null,notes:[],checklist:[],incelendi:false,atananPersonel:null,kanbanStatus:'incelenecek',bidDraft:null})) : [];
} catch {
try { localStorage.removeItem(TENDER_CACHE_KEY); } catch { /* yok say */ }
return [];
@@ -151,16 +153,26 @@ export const AppProvider = ({ children }) => {
useEffect(() => {
if (!authUser) { setVeriSenkron(false); setPlan(null); return; }
planYenile();
getAllUserData()
.then((v) => {
if (v.favori_kurumlar) setFavoriteKurumlar(v.favori_kurumlar);
if (v.ekip) setTeamMembers(v.ekip);
if (v.saved_filters) setSavedFilters(v.saved_filters);
let cancelled = false;
setVeriSenkron(false);
ilkYazmaAtlanan.current.clear();
savedTenderStates.current = {};
Promise.all([getAllUserData(), getTenderStates()])
.then(([v, records]) => {
if (cancelled) return;
setFavoriteKurumlar(v.favori_kurumlar || []);
setTeamMembers(v.ekip || []);
setSavedFilters(v.saved_filters || []);
if (v.reminder) setReminderSettings((s) => ({ ...s, ...v.reminder }));
if (v.rakip_izleme) setCompetitorWatchlist(v.rakip_izleme);
setCompetitorWatchlist(v.rakip_izleme || []);
if (v.firma_profil) setUserProfile(p => ({...p,...v.firma_profil}));
savedTenderStates.current = records;
setTenders(prev => applyTenderStates(prev, records));
setVeriSenkron(true);
setSaveError(null);
})
.catch((e) => console.warn('[veri] sunucudan alınamadı:', e.message))
.finally(() => setVeriSenkron(true));
.catch((e) => { if (!cancelled) setSaveError('Hesap verileri yüklenemedi: ' + e.message); });
return () => { cancelled = true; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [authUser?.email]);
@@ -168,6 +180,38 @@ export const AppProvider = ({ children }) => {
// localStorage yalnızca çevrimdışı önbellek; kaynak doğruluk sunucuda.
const [plan, setPlan] = useState(null);
const [veriSenkron, setVeriSenkron] = useState(false);
const savedTenderStates = useRef({});
const inFlight = useRef(new Set());
const [saveError, setSaveError] = useState(null);
const [savingCount, setSavingCount] = useState(0);
const mutateTender = async (tenderId, patch) => {
const t = tenders.find(t => t.id === tenderId);
if (!t || !veriSenkron || !authUser) { setSaveError('Hesap verileri yüklenmeden kayıt yapılamaz.'); return false; }
const key = tenderStateKey(t);
if (inFlight.current.has(key)) return false;
const owner = authUser.email;
inFlight.current.add(key); setSavingCount(n => n + 1); setSaveError(null);
try {
const record = await saveTenderState(key, patch, tenderSnapshot(t), savedTenderStates.current[key]?.version || 0);
if (getStoredUser()?.email !== owner) return false;
savedTenderStates.current = {...savedTenderStates.current,[key]:record};
setTenders(prev => applyTenderStates(prev, savedTenderStates.current));
return true;
} catch (e) {
setSaveError(e.message);
if (e.kod === 'VERSION_CONFLICT') {
try {
const records = await getTenderStates();
if (getStoredUser()?.email === owner) {
savedTenderStates.current = records;
setTenders(prev => applyTenderStates(prev, records));
}
} catch { /* retain visible error */ }
}
return false;
} finally { inFlight.current.delete(key); setSavingCount(n => n - 1); }
};
// Sunucu verisi yüklendiğinde her effect bir kez tetiklenir; bu ilk turda
// yazmak gereksiz (aynı veri) ve boş istemci state'iyle ezme riski taşır.
const ilkYazmaAtlanan = useRef(new Set());
@@ -182,7 +226,7 @@ export const AppProvider = ({ children }) => {
if (!authUser) return;
setUserData(anahtar, deger).catch((e) => {
if (e.kod === 'KOTA_ASILDI') setKotaUyarisi(e.message);
else console.warn('[veri] sunucuya yazılamadı:', e.message);
else setSaveError('Kaydedilemedi: ' + e.message);
});
};
const [kotaUyarisi, setKotaUyarisi] = useState(null);
@@ -193,6 +237,9 @@ export const AppProvider = ({ children }) => {
const handleLogout = () => {
clearSession();
setAuthUser(null);
savedTenderStates.current = {};
setTenders([]);
setActiveTenderDetail(null);
};
// Canlı ihale verisini (ilan.gov.tr) çeker; kullanıcının kanban/takip durumunu korur.
@@ -201,7 +248,7 @@ export const AppProvider = ({ children }) => {
setLiveDataError(null);
try {
const data = await fetchLiveTenders({ refresh });
setTenders(prev => mergeUserState(data.tenders, prev));
setTenders(prev => applyTenderStates(mergeUserState(data.tenders, prev), savedTenderStates.current));
setLiveDataSource({ source: data.source, fetchedAt: data.fetchedAt });
setLiveStats({
toplamIhale: data.totalCount ?? 0,
@@ -401,6 +448,7 @@ export const AppProvider = ({ children }) => {
const data = await analyzeSpecText({ text, fileName, tender, chain: aiModelConfig.chain, mode });
const result = {
...data.result,
tenderKey: tender?.ikn || tender?.id || null,
mode: data.mode || (mode === 'derin' ? 'derin' : 'standart'),
fileName,
model: `${data.model.label} (${data.model.provider})`,
@@ -564,12 +612,8 @@ export const AppProvider = ({ children }) => {
// gerçek /api/stats verisinden gelir.)
const toggleTrackTender = (tenderId) => {
setTenders(prev => prev.map(t => {
if (t.id === tenderId) {
return { ...t, takipte: !t.takipte };
}
return t;
}));
const t = tenders.find(t => t.id === tenderId);
return mutateTender(tenderId, {takipte: !t?.takipte});
};
// Takipteki İKN'ler sunucuya yazılır: zeyilname/düzeltme ilanı ve sonuç
@@ -589,28 +633,21 @@ export const AppProvider = ({ children }) => {
* Hazırlanıyor" kolonunda düşer, takibe alınır — sonuç yayımlanınca bildirim
* gelir. (Fikir: MOBIT-DURT "ilandan göreve, görevden sonuca".)
*/
const gorevAc = (tenderId, { kontrolListesi = [] } = {}) => {
setTenders((prev) => prev.map((t) => {
if (t.id !== tenderId) return t;
return {
...t,
takipte: true,
kanbanStatus: t.kanbanStatus && t.kanbanStatus !== 'incelenecek' ? t.kanbanStatus : 'teklif_hazirlaniyor',
gorev: {
acildi: new Date().toISOString(),
teslim: t.tarih ? `${t.tarih}${t.saat ? ' ' + t.saat : ''}` : null,
kontrolListesi: kontrolListesi.map((k) => ({ ad: typeof k === 'string' ? k : k.document, tamam: false })),
},
};
}));
notify('Hazırlık görevi açıldı', 'Teslim tarihi ihale saati; pano "Teklif Hazırlanıyor" kolonunda.');
const gorevAc = async (tenderId, { kontrolListesi = [] } = {}) => {
const t = tenders.find(t => t.id === tenderId);
if (!t || t.gorev) return;
const ok = await mutateTender(tenderId, {
takipte: true,
kanbanStatus: t.kanbanStatus && t.kanbanStatus !== 'incelenecek' ? t.kanbanStatus : 'teklif_hazirlaniyor',
gorev: { acildi: new Date().toISOString(), teslim: t.tarih ? `${t.tarih} ${t.saat || ''}`.trim() : null,
kontrolListesi: kontrolListesi.map(k => ({ad:typeof k === 'string' ? k : k.document,tamam:false})) },
});
if (ok) notify('Hazırlık görevi kaydedildi', 'Görev sunucuda saklandı; Kanban panosundan devam edebilirsiniz.');
};
const gorevKontrol = (tenderId, idx) => {
setTenders((prev) => prev.map((t) => {
if (t.id !== tenderId || !t.gorev) return t;
const liste = t.gorev.kontrolListesi.map((k, i) => (i === idx ? { ...k, tamam: !k.tamam } : k));
return { ...t, gorev: { ...t.gorev, kontrolListesi: liste } };
}));
const t = tenders.find(t => t.id === tenderId);
if (!t?.gorev) return;
return mutateTender(tenderId, {gorev:{...t.gorev,kontrolListesi:t.gorev.kontrolListesi.map((k,i) => i === idx ? {...k,tamam:!k.tamam} : k)}});
};
// Sunucu bildirimleri (zeyilname / sonuç): 5 dk'da bir çek, yenileri göster
@@ -655,17 +692,8 @@ export const AppProvider = ({ children }) => {
}, [veriSenkron]);
const updateKanbanStatus = (tenderId, newStatus, assignedUser = null) => {
setTenders(prev => prev.map(t => {
if (t.id === tenderId) {
return {
...t,
kanbanStatus: newStatus,
atananPersonel: assignedUser !== null ? assignedUser : t.atananPersonel,
takipte: true
};
}
return t;
}));
const t = tenders.find(t => t.id === tenderId);
return mutateTender(tenderId, {kanbanStatus:newStatus,atananPersonel:assignedUser ?? t?.atananPersonel ?? null,takipte:true});
};
const addSavedFilter = (newFilter) => {
@@ -793,7 +821,7 @@ export const AppProvider = ({ children }) => {
planYenile,
kotaUyarisi,
setKotaUyarisi,
veriSenkron
veriSenkron, saveError, savingCount, mutateTender
}}>
{children}
</AppContext.Provider>