fix: persist tender preparation and correct calendar export
This commit is contained in:
+77
-49
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user