diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index 07a825a..a116a96 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -1016,3 +1016,10 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö - favori_kurumlar API yazımında okunan içerik sürümü zorunlu; eski sürüm 409 alır. Liste olmayan, boş veya yinelenen kurum girdileri reddedilir. Boş listeye dönmek desteklenir. - 229 otomatik test ve derleme geçti. HTTP testi sürüm zorunluluğu, art arda başarılı kayıt, eski sekme silme isteğinin reddi, hatalı liste ve son favoriyi kaldırmayı doğruladı. - Yeni yıldızın tarayıcı bekleme/hata ve iki sekme kabulü açık. Bu değişiklik bildirim aboneliğini otomatik değiştirmez; abonelik kendi kaydedilmiş kurum kapsamını kullanır. Diğer genel anahtarlardan rakip izleme ve hatırlatma ayarlarının eşzamanlı kayıt davranışı ayrıca incelenmeli. Canlıya yayın yapılmadı. + +### Rakip listesinde kalıcılık ve kaynaksız iddiaların kaldırılması — 6 Eylül 2026 +- Rakip ekleme/çıkarma artık sürümlü sunucu onayını bekler; hata halinde mevcut liste ve ekleme girdisi korunur. Otomatik effect kaydı ve kayıt gerçekleşmeden başarı konfeti kaldırıldı. API geçersiz ve aynı ad/kimlikle yinelenen firmaları reddeder. +- Elle eklenen firmaya sabit %5 pazar payı ve varsayılan sıfır sonuç gösterimi kaldırıldı. Kayıtlı satırlar güncel yüklenen sonuç havuzundaki firma analiziyle eşleşirse bu havuzun verisini gösterir; eşleşme yoksa kazanım, hacim ve pay Veri yok olur. Eski kayıt içindeki sabit/stale ölçüler gösterilmez. +- Sunucuda rakip özel bildirim tüketicisi bulunmadığı doğrulandı. Otomatik bildirimler aktif metni, proaktif uyarı vaadi ve navigasyondaki Canlı Uyar rozeti kaldırıldı. Bildirim kontrolleri etkin değil olarak gösterilir; dış mesaj gönderilmez. Yasaklı sorgu bağlantısı da doğrulanmamış pano-kopyalandı iddiası taşımaz. +- 230 otomatik test ve derleme geçti. Rakip API sürüm zorunluluğu, eski sürüm, bozuk/yinelenen liste, kayıt korunması ve kaldırma test edildi. İzole tarayıcıda 7 kontrol geçti: kayıt, üç ölçüde Veri yok, bildirimlerin pasif olması, yenileme kalıcılığı, yanıltıcı rozetin kalkması, yinelenen ekleme engeli, çıkarma. Kanıt qa-radar-persistence.json. +- Gerçek kazanan kaynaklarıyla eşleşmenin ekonomik doğruluğu, rakip özel bildirim özelliğinin uygulanması ve iki sekmeli rakip UI çakışması bu kabulde tamamlanmadı. Bu değişiklik çalışmayan bildirim özelliğini çalışıyor göstermez. QA sekmesi ve yeni süreç kapatıldı; canlıya yayın yapılmadı. diff --git a/docs/quality/yedek-geri-yukleme.md b/docs/quality/yedek-geri-yukleme.md index e088210..c901169 100644 --- a/docs/quality/yedek-geri-yukleme.md +++ b/docs/quality/yedek-geri-yukleme.md @@ -79,3 +79,5 @@ Kuyruktaki ilanın artık kaynakta bulunmaması, teklif zamanının değişmesi, Ekip kaydı (`ekip`) da aynı içerik sürümü sözleşmesine dahil edildi. Yeni ekip arayüzü sunucu onayını bekler. Eski istemciden sürümsüz ekip yazımı 428 ile reddedilir; frontend/backend birlikte güncellenmelidir. Ekip alanlarının geçersiz veya kimliklerinin yinelenmiş olduğu eski kayıtlar önce düzeltilmelidir; sessiz veri temizliği yapılmaz. Favori kurumlar (`favori_kurumlar`) da sürümlü kullanıcı verisi yazımı ister. Eski istemcinin sürümsüz yazımı 428 döner; frontend/backend birlikte sürümlenmelidir. Bekleyen değişiklikler sunucudan onay gelmeden favori listesinde gösterilmez. + +Rakip izleme (`rakip_izleme`) da içerik sürümü zorunlu anahtarlara dahil edildi. Eski istemcilerde sürümsüz yazım 428 alır; güncel frontend ile aynı sürüm backend kullanılmalıdır. Eski rakip kayıtlarındaki ölçüler yeni arayüzde güncel havuz eşleşmesi yerine kullanılmaz; kayıtlı firma adı/sektör korunur. diff --git a/server/index.js b/server/index.js index 9941314..96273eb 100644 --- a/server/index.js +++ b/server/index.js @@ -1209,7 +1209,9 @@ const server = http.createServer(async (req, res) => { } if(k==='favori_kurumlar' && (!Array.isArray(body.deger) || body.deger.some(name=>typeof name!=='string'||!name.trim()||name.length>1000) || new Set(body.deger.map(name=>name.trim())).size!==body.deger.length)) return json(res,400,{success:false,error:'Favori kurum listesi geçersiz veya yinelenen kurum içeriyor.'}); - const protectedKey=['firma_profil','saved_filters','ekip','favori_kurumlar'].includes(k); + if(k==='rakip_izleme' && (!Array.isArray(body.deger) || body.deger.some(item=>!item || typeof item.id!=='string'||!item.id||typeof item.name!=='string'||!item.name.trim()||item.name.length>1000||(item.sector!=null&&typeof item.sector!=='string')) || new Set(body.deger.map(item=>item.id)).size!==body.deger.length || new Set(body.deger.map(item=>item.name.trim().toLocaleLowerCase('tr-TR'))).size!==body.deger.length)) + return json(res,400,{success:false,error:'Rakip listesi geçersiz veya yinelenen firma içeriyor.'}); + const protectedKey=['firma_profil','saved_filters','ekip','favori_kurumlar','rakip_izleme'].includes(k); if(protectedKey && (!Object.hasOwn(body,'revision') || (body.revision!==null && (typeof body.revision!=='string' || !/^[a-f0-9]{64}$/.test(body.revision))))) return json(res,428,{success:false,kod:'VERSION_REQUIRED',error:'Güncel kayıt sürümü gerekli. Sayfayı yenileyip tekrar deneyin.'}); const revision=store.setData(user.email, k, body.deger, protectedKey ? body.revision : undefined); diff --git a/src/components/TopNavigation.jsx b/src/components/TopNavigation.jsx index c58b7cc..bc08d52 100644 --- a/src/components/TopNavigation.jsx +++ b/src/components/TopNavigation.jsx @@ -60,7 +60,7 @@ export const TopNavigation = () => { { id: 'sinir', label: 'Sınır Değer', icon: Calculator }, { id: 'fiyat', label: 'Fiyat Analizi', icon: TrendingUp }, { id: 'kanban', label: 'Kanban Pano', icon: Kanban, badge: 'Sürükle Takip' }, - { id: 'rakip_radar', label: 'Rakip Radar', icon: Radar, badge: 'Canlı Uyar' }, + { id: 'rakip_radar', label: 'Rakip Radar', icon: Radar }, { id: 'dokuman_deposu', label: 'Doküman Deposu', icon: FolderKanban }, { id: 'analitik_dashboard', label: 'Analitik Dashboard', icon: BarChart3 }, { id: 'kirmizi_bayrak', label: 'Kırmızı Bayrak', icon: ShieldAlert, badge: 'Şeffaflık' }, diff --git a/src/components/tools/CompetitorRadar.jsx b/src/components/tools/CompetitorRadar.jsx index 8ee4217..d0b1ebd 100644 --- a/src/components/tools/CompetitorRadar.jsx +++ b/src/components/tools/CompetitorRadar.jsx @@ -1,11 +1,10 @@ import React, { useState, useMemo } from 'react'; import { useApp } from '../../context/AppContext'; import { Radar, Bell, Plus, ShieldAlert, Award, FileText, CheckCircle2, Trash2, ArrowRight, TrendingUp } from 'lucide-react'; -import confetti from 'canvas-confetti'; import { firmStats, formatTL } from '../../services/analytics'; export const CompetitorRadar = () => { - const { competitorWatchlist, setCompetitorWatchlist, setActiveTab, tenders } = useApp(); + const { competitorWatchlist, saveCompetitorWatchlist, competitorSaving, competitorError, setActiveTab, tenders } = useApp(); const [newCompetitorName, setNewCompetitorName] = useState(''); const [newSector, setNewSector] = useState('Medikal & Tıbbi Malzeme'); @@ -14,40 +13,30 @@ export const CompetitorRadar = () => { const watched = new Set(competitorWatchlist.map((c) => c.name.trim().toLocaleLowerCase('tr-TR'))); const addFromReal = (f) => { if (watched.has(f.name.trim().toLocaleLowerCase('tr-TR'))) return; - setCompetitorWatchlist((prev) => [ - { id: 'cw-' + Date.now(), name: f.name, sector: f.sectors[0] || 'Genel', winCount: f.wins, totalRevenue: f.total, marketShare: f.share, status: 'Aktif İzleniyor', alertOnBid: true, alertOnWin: true }, + saveCompetitorWatchlist((prev) => [ + { id: 'cw-' + crypto.randomUUID(), name: f.name, sector: f.sectors[0] || 'Genel', winCount: f.wins, totalRevenue: f.total, marketShare: f.share, status: 'Aktif İzleniyor', alertOnBid: true, alertOnWin: true }, ...prev, ]); - confetti({ particleCount: 40 }); }; - const handleAddCompetitor = (e) => { + const handleAddCompetitor = async (e) => { e.preventDefault(); - if (!newCompetitorName.trim()) return; + if (!newCompetitorName.trim() || watched.has(newCompetitorName.trim().toLocaleLowerCase('tr-TR'))) return; const newComp = { - id: 'cw-' + Date.now(), - name: newCompetitorName, + id: 'cw-' + crypto.randomUUID(), + name: newCompetitorName.trim(), sector: newSector, - winCount: 0, - totalRevenue: 0, - marketShare: 5, status: 'Aktif İzleniyor', alertOnBid: true, alertOnWin: true }; - setCompetitorWatchlist(prev => [newComp, ...prev]); - setNewCompetitorName(''); - confetti({ particleCount: 50 }); - }; - - const toggleAlert = (id, field) => { - setCompetitorWatchlist(prev => prev.map(c => c.id === id ? { ...c, [field]: !c[field] } : c)); + if(await saveCompetitorWatchlist(prev => [newComp, ...prev]))setNewCompetitorName(''); }; const removeCompetitor = (id) => { - setCompetitorWatchlist(prev => prev.filter(c => c.id !== id)); + saveCompetitorWatchlist(prev => prev.filter(c => c.id !== id)); }; return ( @@ -61,12 +50,12 @@ export const CompetitorRadar = () => {
-

Rakip Radar & Proaktif İhale Uyarısı

+

Rakip Radar

- Proaktif Takip + Kayıtlı Firmalar
-

İzlemeye aldığınız rakip firmalar ihaleye teklif verdiğinde veya kazandığında uygulama içi, e-posta ve Telegram bildirimi alın.

+

Rakip listenizi kaydedin ve yüklenen sonuç/sözleşme verileriyle karşılaştırın. Rakip teklif veya kazanımına özel otomatik bildirim şu anda etkin değil.

@@ -119,8 +108,7 @@ export const CompetitorRadar = () => { )} { try { navigator.clipboard?.writeText(f.name); } catch { /* yoksay */ } }} - title="KİK yasaklılar sorgusunu açar; firma adı panoya kopyalanır (EKAP sorguyu API'den vermiyor)" + title="KİK yasaklılar sorgusunu açar; firma adını sorguya girin" className="bg-slate-100 text-slate-700 border border-slate-200 px-1.5 py-0.5 rounded font-bold hover:bg-slate-200"> ⛔ yasaklı sorgula @@ -134,7 +122,7 @@ export const CompetitorRadar = () => {