fix: align server deadlines and follow consumers with preparation state
This commit is contained in:
+16
-7
@@ -25,7 +25,7 @@ const paraTR = (v, birim) => (v == null ? '' : `${Number(v).toLocaleString('tr-T
|
||||
|
||||
/** Bir kullanıcı için brief içeriği. */
|
||||
export function briefOlustur(email, tenders) {
|
||||
const takip = (store.getData(email, 'takip') || []).map(String);
|
||||
const takip = store.getTrackedTenderKeys(email);
|
||||
const yaklasan = yaklasanIhaleler(email, tenders, 7);
|
||||
const birHafta = Date.now() - 7 * 86400000;
|
||||
const sonuclar = [];
|
||||
@@ -40,11 +40,15 @@ export function briefOlustur(email, tenders) {
|
||||
return { hafta, yaklasan, sonuclar: sonuclar.slice(0, 15), bos: !yaklasan.length && !sonuclar.length };
|
||||
}
|
||||
|
||||
const deadlineText = t => t.son_teklif_zamani
|
||||
? new Date(t.son_teklif_zamani).toLocaleTimeString('tr-TR', { timeZone: 'Europe/Istanbul', hour: '2-digit', minute: '2-digit' }) + ' (Türkiye saati)'
|
||||
: 'saat doğrulanmalı';
|
||||
|
||||
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 : ''})`);
|
||||
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} · ${deadlineText(t)})`);
|
||||
}
|
||||
if (b.sonuclar.length) {
|
||||
s.push('', `🏁 Geçen hafta sonuçlanan takipliler (${b.sonuclar.length}):`);
|
||||
@@ -60,7 +64,7 @@ export function briefHtml(b) {
|
||||
? `<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('⏰ 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)} · ${esc(deadlineText(t))})`))}
|
||||
${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>`;
|
||||
}
|
||||
@@ -88,13 +92,18 @@ export async function haftalikBriefGonder(tenderGetir, { zorla = false } = {}) {
|
||||
return r;
|
||||
}
|
||||
|
||||
/** Saatlik kontrol: Pazartesi ≥08:00 ve bu hafta gönderilmediyse gönder. */
|
||||
export function briefScheduleKey(now = new Date()) {
|
||||
const tr = new Date(now.getTime() + 3 * 3600000);
|
||||
if (tr.getUTCDay() !== 1 || tr.getUTCHours() < 8) return null;
|
||||
return `${tr.getUTCFullYear()}-${tr.getUTCMonth()}-${tr.getUTCDate()}`;
|
||||
}
|
||||
|
||||
/** Saatlik kontrol: Türkiye saatiyle Pazartesi ≥08:00. */
|
||||
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 anahtar = briefScheduleKey();
|
||||
if (!anahtar) return;
|
||||
const d = sonGonderim();
|
||||
if (d.son === anahtar) return;
|
||||
sonGonderimYaz({ son: anahtar });
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* edilmiyordu — herkes her şeyi kullanabiliyordu. Burada gerçekten uygulanır.
|
||||
*/
|
||||
|
||||
import { getUser, documentUsage, listApiKeys, getData } from './user-store.mjs';
|
||||
import { getUser, documentUsage, listApiKeys, getData, getTrackedTenderKeys } from './user-store.mjs';
|
||||
|
||||
export const PLAN_HAKLARI = {
|
||||
ucretsiz: {
|
||||
@@ -61,7 +61,7 @@ export function kullanim(email, opts) {
|
||||
const h = haklar(email, opts);
|
||||
const belge = documentUsage(email);
|
||||
const ekip = (getData(email, 'ekip') || []).length;
|
||||
const takip = (getData(email, 'kanban') || []).filter?.((t) => t?.takipte)?.length ?? null;
|
||||
const takip = getTrackedTenderKeys(email).length;
|
||||
// Yönetici hesapları her zaman tam erişimlidir; bu, satın alınan aboneliği
|
||||
// GİZLEMEMELİ — arayüz ikisini ayrı gösterebilsin diye ikisini de döndürüyoruz.
|
||||
const u = getUser(email);
|
||||
|
||||
+21
-15
@@ -6,6 +6,8 @@
|
||||
// Claude Desktop (mcp-remote) araçları: search_tenders, get_tender,
|
||||
// search_results, search_items, upcoming_deadlines. Bağımlılık yok.
|
||||
import * as store from './user-store.mjs';
|
||||
import { deadlineInfo, tenderDateKey } from '../src/services/tenderCalendar.js';
|
||||
import { applyTenderStates, tenderStateKey } from '../src/services/tenderState.js';
|
||||
import { readHistory, resultsByIkn } from './history-store.mjs';
|
||||
import * as kalemStore from './kalem-store.mjs';
|
||||
|
||||
@@ -25,17 +27,17 @@ export function rssUret({ title, link, description, items }) {
|
||||
|
||||
/** dd.mm.yyyy → Date (ihaleist tarih biçimi). */
|
||||
export function trTarih(s) {
|
||||
const m = /(\d{1,2})[./](\d{1,2})[./](\d{4})/.exec(String(s ?? ''));
|
||||
return m ? new Date(Number(m[3]), Number(m[2]) - 1, Number(m[1])) : null;
|
||||
const m = /^(\d{1,2})[./](\d{1,2})[./](\d{4})$/.exec(String(s ?? '').trim());
|
||||
const key = m && tenderDateKey({ tarih: `${m[1].padStart(2, '0')}.${m[2].padStart(2, '0')}.${m[3]}` });
|
||||
return key ? new Date(key + 'T00:00:00+03:00') : null;
|
||||
}
|
||||
|
||||
/** Liste filtresi — RSS ve MCP ortak. */
|
||||
export function ihaleFiltrele(tenders, { q, il, sektor, sadeceAcik = true, limit = 50 } = {}) {
|
||||
export function ihaleFiltrele(tenders, { q, il, sektor, sadeceAcik = true, limit = 50 } = {}, now = new Date()) {
|
||||
const qq = norm(q); const ill = norm(il); const sek = norm(sektor);
|
||||
const bugun = new Date(); bugun.setHours(0, 0, 0, 0);
|
||||
return tenders
|
||||
.filter((t) => !sadeceAcik || !t.kazananFirma)
|
||||
.filter((t) => !sadeceAcik || !trTarih(t.tarih) || trTarih(t.tarih) >= bugun)
|
||||
.filter((t) => !sadeceAcik || deadlineInfo(t, now).expired !== true)
|
||||
.filter((t) => !ill || norm(t.sehir).includes(ill))
|
||||
.filter((t) => !sek || norm(t.sektor).includes(sek))
|
||||
.filter((t) => !qq || norm(t.baslik).includes(qq) || norm(t.kurum).includes(qq) || String(t.ikn ?? '').includes(qq) ||
|
||||
@@ -99,16 +101,20 @@ export const TOOLS = [
|
||||
const hata = (id, code, message) => ({ jsonrpc: '2.0', id: id ?? null, error: { code, message } });
|
||||
const sonuc = (id, result) => ({ jsonrpc: '2.0', id: id ?? null, result });
|
||||
|
||||
export function yaklasanIhaleler(email, tenders, gun = 14) {
|
||||
const takip = new Set((store.getData(email, 'takip') || []).map(String));
|
||||
const bugun = new Date(); bugun.setHours(0, 0, 0, 0);
|
||||
const sinir = new Date(bugun.getTime() + gun * 86400000);
|
||||
return tenders
|
||||
.filter((t) => t.ikn && takip.has(String(t.ikn)))
|
||||
.map((t) => ({ t, d: trTarih(t.tarih) }))
|
||||
.filter(({ d }) => d && d >= bugun && d <= sinir)
|
||||
.sort((a, b) => a.d - b.d)
|
||||
.map(({ t, d }) => ({ ikn: t.ikn, baslik: t.baslik, kurum: t.kurum, sehir: t.sehir, tarih: t.tarih, saat: t.saat, kalan_gun: Math.round((d - bugun) / 86400000), kaynakUrl: t.kaynakUrl }));
|
||||
export function yaklasanIhaleler(email, tenders, gun = 14, now = new Date()) {
|
||||
const states = store.getTenderStates(email);
|
||||
const takip = new Set(store.getTrackedTenderKeys(email, states));
|
||||
const days = Math.min(90, Math.max(1, Math.floor(Number(gun) || 14)));
|
||||
return applyTenderStates(tenders, states)
|
||||
.filter(t => !t.kazananFirma && takip.has(tenderStateKey(t)))
|
||||
.map(t => ({ t, info: deadlineInfo(t, now) }))
|
||||
.filter(({ info }) => info.days !== null && info.days >= 0 && info.days <= days && info.expired !== true)
|
||||
.sort((a, b) => a.info.days - b.info.days ||
|
||||
(a.info.deadline?.getTime() ?? Infinity) - (b.info.deadline?.getTime() ?? Infinity))
|
||||
.map(({ t, info }) => ({ ikn: t.ikn, baslik: t.baslik, kurum: t.kurum, sehir: t.sehir,
|
||||
tarih: t.tarih, saat: t.saat, kalan_gun: info.days,
|
||||
son_teklif_zamani: info.deadline?.toISOString() ?? null,
|
||||
saat_dogrulanmali: !info.deadline, kaynakUrl: t.kaynakUrl }));
|
||||
}
|
||||
|
||||
export async function aracCalistir(name, args, user, ctx) {
|
||||
|
||||
+2
-8
@@ -1,3 +1,4 @@
|
||||
import { deadlineInfo } from '../src/services/tenderCalendar.js';
|
||||
import { exchangeRates } from './exchange-rates.mjs';
|
||||
import { uniqueTenders, announcementFields } from './tender-quality.mjs';
|
||||
/**
|
||||
@@ -152,13 +153,6 @@ const bugunEklenenSayisi = (liste) => {
|
||||
return (liste || []).filter((t) => t.kaynak === 'ilan.gov.tr' && t.ilanTarihi === bugun).length;
|
||||
};
|
||||
|
||||
function daysUntil(ddmmyyyy) {
|
||||
if (!ddmmyyyy) return null;
|
||||
const [d, m, y] = ddmmyyyy.split('.').map(Number);
|
||||
if (!d || !m || !y) return null;
|
||||
return Math.ceil((new Date(y, m - 1, d) - new Date()) / 86400000);
|
||||
}
|
||||
|
||||
// ilan.gov.tr kaydını ihaleist tender modeline çevir
|
||||
function mapAd(ad) {
|
||||
const cityUpper = (ad.addressCityName || '').toLocaleUpperCase('tr-TR');
|
||||
@@ -171,7 +165,7 @@ function mapAd(ad) {
|
||||
const usul = getFilter(ad, 'İhale Usulü') || 'Belirtilmemiş';
|
||||
const ihaleTuru = getFilter(ad, 'İhale Türü') || '';
|
||||
const searchBase = `${ad.title || ''} ${ad.advertiserName || ''}`; // yalnızca arama için
|
||||
const kalanGun = daysUntil(tarih);
|
||||
const kalanGun = deadlineInfo({ tarih }).days;
|
||||
|
||||
let aciliyetBadge = null;
|
||||
if (kalanGun !== null && kalanGun >= 0 && kalanGun <= 3) aciliyetBadge = '⚠️ SON 3 GÜN';
|
||||
|
||||
@@ -294,6 +294,16 @@ export function getTenderStates(email) {
|
||||
return withDb(db => Object.fromEntries(db.prepare('SELECT * FROM tender_state WHERE email=?').all(email)
|
||||
.map(r => [r.tender_key, { data: JSON.parse(r.data), snapshot: JSON.parse(r.snapshot), version: r.version }])));
|
||||
}
|
||||
// Current per-tender state overrides legacy mirrored follow lists, including unfollow.
|
||||
export function getTrackedTenderKeys(email, states = getTenderStates(email)) {
|
||||
const legacy = getData(email, 'takip');
|
||||
const keys = new Set((Array.isArray(legacy) ? legacy : []).map(String));
|
||||
for (const [key, record] of Object.entries(states)) {
|
||||
if (record.data.takipte === true) keys.add(key);
|
||||
else if (record.data.takipte === false) keys.delete(key);
|
||||
}
|
||||
return [...keys];
|
||||
}
|
||||
export function saveTenderState(email, key, patch, snapshot, expectedVersion) {
|
||||
const fields = new Set(['takipte','incelendi','kanbanStatus','atananPersonel','gorev','notes','checklist','bidDraft']);
|
||||
if (!key || key.length > 100 || !patch || typeof patch !== 'object' || Array.isArray(patch)
|
||||
|
||||
@@ -59,7 +59,7 @@ export function zeyilnameDurumu() { return { calisiyor, sonTur }; }
|
||||
function takipHaritasi() {
|
||||
const harita = new Map();
|
||||
for (const u of store.listUsers()) {
|
||||
const liste = store.getData(u.email, 'takip');
|
||||
const liste = store.getTrackedTenderKeys(u.email);
|
||||
if (!Array.isArray(liste)) continue;
|
||||
for (const ikn of liste) {
|
||||
const k = String(ikn);
|
||||
|
||||
Reference in New Issue
Block a user