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
+62
View File
@@ -26,6 +26,8 @@ import { kalemleriCikar } from './kalem-cikar.mjs';
import { searchInDocuments, searchByOkas, okasAra } from './ekap-v2.mjs';
import { iknCikar } from './kalem-cikar.mjs';
import { bonusHesapla, gommeDurumu, arkaPlandaIsit } from './uyum-gomme.mjs';
import { telegramConfigured, botAdi, kodIleBagla, sendTelegram } from './telegram.mjs';
import { bildirimleriOku, okunduIsaretle, zeyilnameDurumu, zeyilnameTara, sonucTara } from './zeyilname.mjs';
// Ürün araması EKAP'a uzak çağrı yapar; aynı sorgu tekrarlanırsa kaynağı yorma.
const urunAramaCache = new Map();
@@ -818,6 +820,52 @@ const server = http.createServer(async (req, res) => {
if (url.pathname === '/api/health/uyum') {
return json(res, 200, { success: true, ...gommeDurumu() });
}
// --- Telegram bağlantısı (kullanıcı kodu bota yazar, biz getUpdates'te görürüz) ---
if (url.pathname.startsWith('/api/me/telegram')) {
const user = userFromAuthHeader(req.headers['authorization']);
if (!user) return json(res, 401, { success: false, error: 'oturum yok' });
const tg = store.getData(user.email, 'telegram');
if (req.method === 'GET') {
return json(res, 200, { success: true, yapilandirilmis: telegramConfigured(), bot: await botAdi(), bagli: !!tg?.chatId, ad: tg?.ad || null, baglanma: tg?.baglanma || null });
}
if (req.method === 'DELETE') { store.setData(user.email, 'telegram', null); return json(res, 200, { success: true }); }
if (req.method === 'POST') {
try {
const body = await readBody(req);
if (body.islem === 'test') {
if (!tg?.chatId) return json(res, 400, { success: false, error: 'Telegram bağlı değil' });
await sendTelegram(tg.chatId, '✅ İhaleist bağlantı testi — bildirimler bu sohbete gelecek.');
return json(res, 200, { success: true });
}
const kod = String(body.kod || '').replace(/\D/g, '');
if (kod.length !== 6) return json(res, 400, { success: false, error: '6 haneli kod gerekli' });
const bulunan = await kodIleBagla(kod);
if (!bulunan) return json(res, 404, { success: false, error: 'Kod bota gönderilmiş görünmüyor. Önce bota yazın, sonra doğrulayın.' });
store.setData(user.email, 'telegram', { ...bulunan, baglanma: new Date().toISOString() });
await sendTelegram(bulunan.chatId, `✅ İhaleist hesabınız (${user.email}) bu sohbete bağlandı.`).catch(() => {});
return json(res, 200, { success: true, ad: bulunan.ad });
} catch (e) { return json(res, 502, { success: false, error: e.message }); }
}
}
// --- Takip bildirimleri (zeyilname / sonuç) ---
if (url.pathname === '/api/me/bildirimler') {
const user = userFromAuthHeader(req.headers['authorization']);
if (!user) return json(res, 401, { success: false, error: 'oturum yok' });
if (req.method === 'POST') {
const body = await readBody(req);
return json(res, 200, { success: true, okundu: okunduIsaretle(user.email, body.idler || []) });
}
return json(res, 200, { success: true, bildirimler: bildirimleriOku(user.email) });
}
if (url.pathname === '/api/health/takip') {
return json(res, 200, { success: true, ...zeyilnameDurumu(), telegram: telegramConfigured() });
}
if (url.pathname === '/api/admin/takip/tara' && req.method === 'POST') {
const user = userFromAuthHeader(req.headers['authorization']);
if (!user?.isAdmin) return json(res, 403, { success: false, error: 'yönetici gerekli' });
const [z, so] = await Promise.all([zeyilnameTara(), sonucTara()]);
return json(res, 200, { success: true, zeyilname: z, sonuc: so });
}
if (url.pathname === '/api/health/kalem') {
return json(res, 200, { success: true, ...kalemStore.kalemSayilari(), depo: kalemStore.available() });
}
@@ -971,6 +1019,20 @@ const server = http.createServer(async (req, res) => {
return json(res, 500, { success: false, error: 'dosya okunamadı' });
}
}
// Belge üst verisi: son kullanma tarihi (süre takibi) / kategori
if (url.pathname === '/api/me/documents/meta' && req.method === 'PATCH') {
const user = userFromAuthHeader(req.headers['authorization']);
if (!user) return json(res, 401, { success: false, error: 'oturum yok' });
try {
const body = await readBody(req);
if (!body.id) return json(res, 400, { success: false, error: 'id gerekli' });
const sonKullanma = body.sonKullanma ? String(body.sonKullanma).slice(0, 10) : null;
if (sonKullanma && !/^\d{4}-\d{2}-\d{2}$/.test(sonKullanma)) return json(res, 400, { success: false, error: 'tarih YYYY-AA-GG olmalı' });
const ok = store.updateDocument(user.email, String(body.id), { sonKullanma, kategori: body.kategori ? String(body.kategori).slice(0, 60) : null });
if (!ok) return json(res, 404, { success: false, error: 'belge bulunamadı' });
return json(res, 200, { success: true, belgeler: store.listDocuments(user.email) });
} catch (e) { return json(res, 400, { success: false, error: e.message }); }
}
// Belge metni — docx/xlsx/zip için. PDF tarayıcıda (pdfjs) okunuyor;
// bu türler için tarayıcıda çözücü yok, baytlar zaten burada duruyor.
if (url.pathname === '/api/me/documents/text') {