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
+40 -2
View File
@@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef } from 'react';
import { useApp } from '../../context/AppContext';
import { listDocuments, uploadDocument, deleteDocument, downloadDocument, formatBayt } from '../../services/account';
import { FolderKanban, Upload, Trash2, Download, Loader2, AlertTriangle, FileText, HardDrive, Layers, Package, CheckCircle2, XCircle } from 'lucide-react';
import { listDocuments, uploadDocument, deleteDocument, downloadDocument, formatBayt, updateDocumentMeta } from '../../services/account';
import { FolderKanban, Upload, Trash2, Download, Loader2, AlertTriangle, FileText, HardDrive, Layers, Package, CheckCircle2, XCircle, CalendarClock } from 'lucide-react';
const KATEGORILER = ['Yasal Belgeler', 'Kalite Belgeleri', 'Mali Belgeler', 'Resmi Kayıtlar', 'Teknik Belgeler', 'Diğer'];
@@ -82,6 +82,17 @@ export const DocumentVault = () => {
if (topluRef.current) topluRef.current.value = '';
};
// Şirket belgesi süre takibi: imza sirküleri, oda kayıt, borcu yoktur —
// hepsinin süresi dolar, kimse teklif hazırlanana kadar fark etmez.
const sureYaz = async (id, tarih) => {
setHata(null);
try { setDurum(await updateDocumentMeta(id, { sonKullanma: tarih || null })); }
catch (e) { setHata(e.message); }
};
const kalanGun = (b) => (b.son_kullanma ? Math.ceil((new Date(b.son_kullanma).getTime() - Date.now()) / 86400000) : null);
const suresiDolan = (durum?.belgeler || []).filter((b) => kalanGun(b) != null && kalanGun(b) < 0);
const yakinda = (durum?.belgeler || []).filter((b) => kalanGun(b) != null && kalanGun(b) >= 0 && kalanGun(b) <= 30);
const sil = async (id) => {
setBusy(true); setHata(null);
try { setDurum(await deleteDocument(id)); }
@@ -154,6 +165,22 @@ export const DocumentVault = () => {
</div>
</div>
{/* Süre uyarısı */}
{(suresiDolan.length > 0 || yakinda.length > 0) && (
<div className={`rounded-2xl border-2 p-4 flex gap-3 ${suresiDolan.length ? 'bg-red-50 border-red-300' : 'bg-amber-50 border-amber-300'}`}>
<CalendarClock className={`w-5 h-5 shrink-0 mt-0.5 ${suresiDolan.length ? 'text-red-600' : 'text-amber-600'}`} />
<div className="text-xs leading-relaxed">
{suresiDolan.length > 0 && (
<p className="font-bold text-red-900">Süresi dolmuş: {suresiDolan.map((b) => b.ad).join(', ')}</p>
)}
{yakinda.length > 0 && (
<p className="font-semibold text-amber-900">30 gün içinde doluyor: {yakinda.map((b) => `${b.ad} (${kalanGun(b)} gün)`).join(', ')}</p>
)}
<p className="text-slate-600 mt-1">Teklif zarfına süresi dolmuş belge girerse teklif değerlendirme dışı kalır. Yenileyip yeni tarihi girin.</p>
</div>
</div>
)}
{/* Toplu ihale dokümanı yükleme */}
<div className="bg-linear-to-r from-slate-900 via-indigo-950 to-slate-900 rounded-2xl p-5 space-y-3">
<div className="flex items-start gap-3">
@@ -226,6 +253,17 @@ export const DocumentVault = () => {
<p className="text-[10px] text-slate-400 mt-0.5">
{new Date(d.olusturma).toLocaleString('tr-TR')}
</p>
<label className="flex items-center gap-1.5 mt-1.5 text-[10px] text-slate-500">
<CalendarClock className={`w-3 h-3 ${kalanGun(d) != null && kalanGun(d) < 0 ? 'text-red-600' : kalanGun(d) != null && kalanGun(d) <= 30 ? 'text-amber-600' : 'text-slate-400'}`} />
<span>Son kullanma</span>
<input type="date" value={d.son_kullanma || ''} onChange={(e) => sureYaz(d.id, e.target.value)}
className="border border-slate-200 rounded px-1 py-0.5 text-[10px]" />
{kalanGun(d) != null && (
<span className={`font-bold ${kalanGun(d) < 0 ? 'text-red-700' : kalanGun(d) <= 30 ? 'text-amber-700' : 'text-emerald-700'}`}>
{kalanGun(d) < 0 ? `${-kalanGun(d)} gün önce doldu` : `${kalanGun(d)} gün kaldı`}
</span>
)}
</label>
</div>
<div className="flex items-center gap-1 shrink-0">
<button onClick={() => downloadDocument(d.id, d.ad).catch((e) => setHata(e.message))}