feat: takip izleme (zeyilname + sonuc bildirimi), hazirlik gorevi, belge sure uyarisi, Telegram, OCR, yasakli sorgu
28 depo taramasindan alinan alti ozellik. Kod kopyalanmadi (depolar lisanssiz), fikirler uygulandi. 1) Zeyilname / duzeltme ilani takibi (server/zeyilname.mjs): takipteki IKN'ler sunucuya yazilir; 3 saatte bir EKAP detayindan ilan sayisi ve ihale tarihi karsilastirilir, degisince "ne degisti" bildirimi (uygulama ici + e-posta + Telegram). Test: anlik kaydi eskitilince tur "yeni ilan yayimlandi; ihale tarihi 01.01.2026 → 08.10.2026" bildirimi uretti. 2) Sonuc bildirimi: takipteki ihale sonuclaninca "X firmasi Y'ye aldi, %Z kirim". Yalniz takiptekiler; kismi ihalede oran yazilmaz. 3) Ihaleden goreve (MOBIT-DURT): detayda "Hazirlik Gorevi Ac" → teslim tarihi ihale saati, pano "Teklif Hazirlaniyor", derin analizden gelen belge kontrol listesi gorevde; Kanban kartinda geri sayim rozeti. 4) Sirket belgesi sure takibi: Belge Kasasi'nda son kullanma tarihi girisi, dolmus / 30 gun icinde dolacak uyarisi; girise gunde bir bildirim. PATCH /api/me/documents/meta. 5) Telegram (server/telegram.mjs): TELEGRAM_BOT_TOKEN sunucuda; kullanici 6 haneli kodu bota yazar, "Dogrula" → getUpdates ile chat id baglanir. Arayuzde adim adim kurulum rehberi (BotFather → env → Redeploy → kod). Ihale ozeti, zeyilname ve sonuc bildirimleri Telegram'a da gider. 6) OCR (tesseract.js, tur): PDF metin katmani bossa (taranmis) ilk 8 sayfa OCR. Olcum: sentetik cetvelde guven 87, ~0,3 sn/sayfa; IKN ve kalemler dogru okundu. Kalem havuzu ve derin analiz artik taranmis PDF'ten de besleniyor. 7) Rakip Radari'nda KIK yasakli sorgusu baglantisi (firma adi panoya kopyalanir; EKAP sorguyu API'den vermiyor). Sinir deger hesaplayici KURULMADI: Kamu Ihale Genel Tebligi'ne ulasilamadi (mevzuat.gov.tr / ihale.gov.tr bu makineden erisilemiyor); formul kaynaksiz yazilmadi. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { havuzuPuanla, profilMetni, profilTerimleri } from '../services/uyum';
|
||||
import { getToken } from '../services/auth';
|
||||
import { fetchLiveTenders, fetchTenderDetail, mergeUserState } from '../services/tenderApi';
|
||||
import { restoreSession, clearSession, getStoredUser } from '../services/auth';
|
||||
import { getAllUserData, setUserData, getPlan } from '../services/account';
|
||||
import { getAllUserData, setUserData, getPlan, bildirimleriGetir, bildirimOkundu, listDocuments } from '../services/account';
|
||||
import { SECTORS_LIST, INSTITUTION_TYPES_LIST } from '../data/constants';
|
||||
|
||||
const AppContext = createContext();
|
||||
@@ -563,6 +563,88 @@ export const AppProvider = ({ children }) => {
|
||||
}));
|
||||
};
|
||||
|
||||
// Takipteki İKN'ler sunucuya yazılır: zeyilname/düzeltme ilanı ve sonuç
|
||||
// bildirimi sunucudaki zamanlayıcıda üretiliyor, tarayıcı kapalıyken de.
|
||||
const takipIknler = useMemo(
|
||||
() => tenders.filter((t) => t.takipte || t.gorev).map((t) => t.ikn).filter(Boolean).sort(),
|
||||
[tenders],
|
||||
);
|
||||
const takipImza = takipIknler.join('|');
|
||||
useEffect(() => {
|
||||
if (veriSenkron) sunucuyaYaz('takip', takipIknler);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [takipImza, veriSenkron]);
|
||||
|
||||
/**
|
||||
* İhaleden hazırlık görevi: teslim tarihi ihale saatidir, panoya "Teklif
|
||||
* 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 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 } };
|
||||
}));
|
||||
};
|
||||
|
||||
// Sunucu bildirimleri (zeyilname / sonuç): 5 dk'da bir çek, yenileri göster
|
||||
useEffect(() => {
|
||||
if (!veriSenkron) return;
|
||||
let iptal = false;
|
||||
const cek = async () => {
|
||||
try {
|
||||
const liste = await bildirimleriGetir();
|
||||
const yeni = liste.filter((b) => !b.okundu);
|
||||
if (iptal || !yeni.length) return;
|
||||
for (const b of yeni.slice(0, 5)) notify(b.baslik, b.metin);
|
||||
await bildirimOkundu(yeni.map((b) => b.id));
|
||||
} catch { /* sessiz */ }
|
||||
};
|
||||
cek();
|
||||
const z = setInterval(cek, 5 * 60 * 1000);
|
||||
return () => { iptal = true; clearInterval(z); };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [veriSenkron]);
|
||||
|
||||
// Şirket belgesi süre uyarısı: süresi dolmuş / 30 gün içinde dolacak
|
||||
// belgeler için günde bir kez bildirim (kimse teklif hazırlanana kadar
|
||||
// fark etmiyor — fikir: MOBIT-DURT).
|
||||
useEffect(() => {
|
||||
if (!veriSenkron) return;
|
||||
const bugun = new Date().toISOString().slice(0, 10);
|
||||
try { if (localStorage.getItem('ihaleist_belge_uyari_gunu') === bugun) return; } catch { /* yoksay */ }
|
||||
listDocuments().then((d) => {
|
||||
const simdi = Date.now();
|
||||
const dolan = [], yakin = [];
|
||||
for (const b of d?.belgeler || []) {
|
||||
if (!b.son_kullanma) continue;
|
||||
const kalan = Math.ceil((new Date(b.son_kullanma).getTime() - simdi) / 86400000);
|
||||
if (kalan < 0) dolan.push(b.ad); else if (kalan <= 30) yakin.push(`${b.ad} (${kalan} gün)`);
|
||||
}
|
||||
if (dolan.length) notify(`${dolan.length} belgenin süresi dolmuş`, dolan.slice(0, 4).join(', '));
|
||||
if (yakin.length) notify(`${yakin.length} belgenin süresi 30 gün içinde doluyor`, yakin.slice(0, 4).join(', '));
|
||||
try { localStorage.setItem('ihaleist_belge_uyari_gunu', bugun); } catch { /* yoksay */ }
|
||||
}).catch(() => {});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [veriSenkron]);
|
||||
|
||||
const updateKanbanStatus = (tenderId, newStatus, assignedUser = null) => {
|
||||
setTenders(prev => prev.map(t => {
|
||||
if (t.id === tenderId) {
|
||||
@@ -695,6 +777,8 @@ export const AppProvider = ({ children }) => {
|
||||
removeTeamMember,
|
||||
toggleTrackTender,
|
||||
updateKanbanStatus,
|
||||
gorevAc,
|
||||
gorevKontrol,
|
||||
clearAllFilters,
|
||||
plan,
|
||||
planYenile,
|
||||
|
||||
Reference in New Issue
Block a user