Files
ihaleist/server/brief.mjs
T
hikmettanandClaude Fable 5.1 48f0c48732 feat: İhaleAB'den taşınan özellikler — MCP sunucusu, RSS, haftalık brief, teklif asistanı, yeterlilik rozeti, 5 yeni kırmızı bayrak
- /mcp (JSON-RPC, ih_live_ anahtarı): search_tenders, get_tender, search_results, search_items, upcoming_deadlines
- /api/feeds/tenders.rss (anahtarlı) ve /api/feeds/sonuc.rss (açık)
- Haftalık brief (Pazartesi 08:00, Telegram + e-posta), /api/me/brief tercihi + önizleme, sağ panelde yaklaşan ihale tarihleri
- Detayda Teklif Asistanı sekmesi: belgeye sor (atıflı), taslak denetimi, taslak üretimi (analyzeSpec zinciri)
- Derin analiz belge listesi × Doküman Deposu eşleştirme rozeti
- Kırmızı bayrak: kısa süre, hafta sonu tarih, geç sözleşme, kazananı gizli, DT parçalama şüphesi
- Kullanım kılavuzu güncellendi

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 02:23:41 +03:00

108 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Haftalık brief — Pazartesi 08:00: takipteki ihalelerden bu hafta yapılacaklar,
// geçen hafta sonuçlanan takipliler, takiplilere gelen zeyilname sayısı.
// Telegram (bağlıysa) + e-posta. Son gönderim data/brief-son.json'da tutulur;
// saatlik kontrol, haftada bir gönderim (node-cron yok, setInterval).
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import * as store from './user-store.mjs';
import { resultsByIkn } from './history-store.mjs';
import { emailConfigured, sendEmail } from './email.mjs';
import { sendTelegram, telegramConfigured } from './telegram.mjs';
import { yaklasanIhaleler, trTarih } from './feeds-mcp.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DURUM_DOSYASI = path.join(__dirname, 'data', 'brief-son.json');
function sonGonderim() {
try { return JSON.parse(fs.readFileSync(DURUM_DOSYASI, 'utf8')); } catch { return {}; }
}
function sonGonderimYaz(d) {
try { fs.mkdirSync(path.dirname(DURUM_DOSYASI), { recursive: true }); fs.writeFileSync(DURUM_DOSYASI, JSON.stringify(d)); } catch { /* yok */ }
}
const paraTR = (v, birim) => (v == null ? '' : `${Number(v).toLocaleString('tr-TR')} ${birim || 'TL'}`);
/** Bir kullanıcı için brief içeriği. */
export function briefOlustur(email, tenders) {
const takip = (store.getData(email, 'takip') || []).map(String);
const yaklasan = yaklasanIhaleler(email, tenders, 7);
const birHafta = Date.now() - 7 * 86400000;
const sonuclar = [];
for (const ikn of takip.slice(0, 200)) {
const r = resultsByIkn(ikn) || [];
for (const s of r) {
const d = trTarih(s.sozlesmeTarihi) || trTarih(s.tarih);
if (d && d.getTime() >= birHafta) sonuclar.push({ ikn, baslik: s.baslik, kazanan: s.kazananFirma, bedel: s.sozlesmeBedeli, birim: s.paraBirimi, teklif: s.teklifSayisi });
}
}
const hafta = new Date().toLocaleDateString('tr-TR', { day: '2-digit', month: 'long', year: 'numeric' });
return { hafta, yaklasan, sonuclar: sonuclar.slice(0, 15), bos: !yaklasan.length && !sonuclar.length };
}
export function briefMetni(b) {
const s = [`📋 Haftalık İhale Brief — ${b.hafta}`];
if (b.yaklasan.length) {
s.push('', `⏰ Bu hafta yapılacak takipli ihaleler (${b.yaklasan.length}):`);
for (const t of b.yaklasan) s.push(`• ${t.kalan_gun === 0 ? 'BUGÜN' : t.kalan_gun + ' gün'} — ${t.baslik} (${t.kurum}, ${t.tarih}${t.saat ? ' ' + t.saat : ''})`);
}
if (b.sonuclar.length) {
s.push('', `🏁 Geçen hafta sonuçlanan takipliler (${b.sonuclar.length}):`);
for (const r of b.sonuclar) s.push(`• ${r.baslik} → ${r.kazanan || 'kazanan açıklanmadı'}${r.bedel != null ? ' · ' + paraTR(r.bedel, r.birim) : ''}${r.teklif != null ? ' · ' + r.teklif + ' teklif' : ''}`);
}
s.push('', 'ihaleist.com');
return s.join('\n');
}
export function briefHtml(b) {
const esc = (x) => String(x ?? '').replace(/[<>&]/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;' }[c]));
const bolum = (baslik, satirlar) => satirlar.length
? `<h3 style="margin:18px 0 6px;font-size:15px;color:#0f172a">${baslik}</h3><ul style="margin:0;padding-left:18px;color:#334155;font-size:14px">${satirlar.map((x) => `<li style="margin:4px 0">${x}</li>`).join('')}</ul>` : '';
return `<div style="font-family:Inter,Arial,sans-serif;max-width:640px;margin:auto;padding:20px;color:#0f172a">
<h2 style="margin:0 0 4px;font-size:20px">📋 Haftalık İhale Brief</h2><div style="color:#64748b;font-size:13px">${esc(b.hafta)}</div>
${bolum('⏰ Bu hafta yapılacak takipli ihaleler', b.yaklasan.map((t) => `<b>${t.kalan_gun === 0 ? 'BUGÜN' : t.kalan_gun + ' gün'}</b> — ${esc(t.baslik)} (${esc(t.kurum)}, ${esc(t.tarih)})`))}
${bolum('🏁 Geçen hafta sonuçlanan takipliler', b.sonuclar.map((r) => `${esc(r.baslik)} → ${esc(r.kazanan || 'kazanan açıklanmadı')}${r.bedel != null ? ' · ' + esc(paraTR(r.bedel, r.birim)) : ''}`))}
<p style="margin-top:20px;font-size:12px;color:#94a3b8">Bu brief her Pazartesi gönderilir; Bildirimler sekmesinden kapatabilirsiniz.</p></div>`;
}
export async function haftalikBriefGonder(tenderGetir, { zorla = false } = {}) {
const tenders = await tenderGetir();
const r = { kullanici: 0, gonderilen: 0, bos: 0, hata: 0 };
for (const u of store.listUsers()) {
r.kullanici++;
try {
const tercih = store.getData(u.email, 'brief');
if (tercih && tercih.kapali && !zorla) continue;
const b = briefOlustur(u.email, tenders);
if (b.bos) { r.bos++; continue; }
const tg = store.getData(u.email, 'telegram');
let gitti = false;
if (telegramConfigured() && tg?.chatId) gitti = await sendTelegram(tg.chatId, briefMetni(b).slice(0, 3900)).then(() => true).catch(() => false);
if (emailConfigured()) gitti = (await sendEmail({ to: u.email, subject: `İhaleist — Haftalık Brief (${b.hafta})`, html: briefHtml(b), text: briefMetni(b) }).then(() => true).catch(() => false)) || gitti;
if (gitti) r.gonderilen++;
} catch (e) {
r.hata++;
console.warn('[brief]', u.email, e.message);
}
}
return r;
}
/** Saatlik kontrol: Pazartesi ≥08:00 ve bu hafta gönderilmediyse gönder. */
export function briefZamanlayici(tenderGetir) {
if (process.env.BRIEF_ENABLED === '0') { console.log('[brief] haftalık brief devre dışı (BRIEF_ENABLED=0)'); return; }
const kontrol = async () => {
const simdi = new Date();
if (simdi.getDay() !== 1 || simdi.getHours() < 8) return;
const anahtar = `${simdi.getFullYear()}-${simdi.getMonth()}-${simdi.getDate()}`;
const d = sonGonderim();
if (d.son === anahtar) return;
sonGonderimYaz({ son: anahtar });
const r = await haftalikBriefGonder(tenderGetir).catch((e) => ({ hata: e.message }));
console.log('[brief] haftalık brief:', JSON.stringify(r));
};
setTimeout(kontrol, 3 * 60 * 1000).unref?.();
setInterval(kontrol, 60 * 60 * 1000).unref?.();
console.log('[brief] haftalık brief zamanlandı (Pazartesi 08:00, saatlik kontrol)');
}