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:
2026-09-03 22:58:13 +03:00
co-authored by Claude Fable 5.1
parent 1716b6365c
commit df89eaac33
18 changed files with 802 additions and 10 deletions
+85 -1
View File
@@ -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,