feat: tüm mock veriyi kaldır + Resend e-posta entegrasyonu
- mockTenders.js silindi; referans listeleri (sektör/il/kurum) constants.js'e taşındı - AppContext: tenders/liveStats/competitorWatchlist/vaultDocuments/notifications/ savedFilters/apiKeys/scrapedCapabilities → gerçek/boş; sahte Math.random online sayacı kaldırıldı; competitor & belge kasası artık localStorage - LiveStatsBar: gerçek alanlar (canlı havuz/yüklü/bugün/kalem/takibimdeki) - AnalyticsDashboard: KPI'lar (katılım/kazanma oranı/kazanılan bedel) + aylık hacim + sektörel dağılım gerçek companyStats/monthlyStats/sectorStats'tan; ciro hedefi kullanıcı tarafından belirlenir; veri yoksa dürüst boş durum - ContractSearch & Rakip Radar: gerçek firmStats - analytics.js: companyStats/monthlyStats/parseDateTR eklendi - Resend: server/email.mjs + /api/notify/email/config + /api/notify/email/test (auth'lu, kullanıcının kendi adresine); PwaSettings gerçek e-posta test kartı
This commit is contained in:
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Resend e-posta gönderimi — bağımlılık yok (Node 18+ built-in fetch).
|
||||||
|
*
|
||||||
|
* Env:
|
||||||
|
* RESEND_API_KEY — Resend API anahtarı (re_...). Yoksa e-posta kapalıdır.
|
||||||
|
* EMAIL_FROM — Gönderen adres. Üretimde Resend'de DNS (SPF/DKIM) ile
|
||||||
|
* doğrulanmış kendi domaininiz olmalı. Doğrulanana kadar
|
||||||
|
* Resend'in paylaşımlı test adresi kullanılır.
|
||||||
|
*
|
||||||
|
* Kaynak/Endpoint: POST https://api.resend.com/emails (Bearer auth)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const RESEND_ENDPOINT = 'https://api.resend.com/emails';
|
||||||
|
|
||||||
|
export function emailConfigured() {
|
||||||
|
return Boolean(process.env.RESEND_API_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emailFrom() {
|
||||||
|
return process.env.EMAIL_FROM || 'İhaleist <onboarding@resend.dev>';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendEmail({ to, subject, html, text }) {
|
||||||
|
const key = process.env.RESEND_API_KEY;
|
||||||
|
if (!key) throw new Error('E-posta yapılandırılmamış (RESEND_API_KEY yok)');
|
||||||
|
if (!to || !subject || (!html && !text)) throw new Error('Eksik alan: to, subject ve html/text gerekli');
|
||||||
|
|
||||||
|
const res = await fetch(RESEND_ENDPOINT, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${key}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ from: emailFrom(), to: Array.isArray(to) ? to : [to], subject, html, text }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(data?.message || `Resend hatası: HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
return { id: data.id };
|
||||||
|
}
|
||||||
|
|
||||||
|
// İhale bildirimi için basit, markasız HTML gövdesi
|
||||||
|
export function tenderDigestHtml({ name, tenders }) {
|
||||||
|
const rows = (tenders || [])
|
||||||
|
.slice(0, 15)
|
||||||
|
.map(
|
||||||
|
(t) => `<tr>
|
||||||
|
<td style="padding:8px 12px;border-bottom:1px solid #eef1f5;font-size:13px;color:#0e1526">${escapeHtml(t.baslik || '')}</td>
|
||||||
|
<td style="padding:8px 12px;border-bottom:1px solid #eef1f5;font-size:12px;color:#586074;white-space:nowrap">${escapeHtml(t.sehir || '')}</td>
|
||||||
|
<td style="padding:8px 12px;border-bottom:1px solid #eef1f5;font-size:12px;color:#586074;white-space:nowrap">${escapeHtml(t.tarih || '')}</td>
|
||||||
|
</tr>`
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
return `<div style="font-family:Arial,Helvetica,sans-serif;max-width:640px;margin:0 auto">
|
||||||
|
<h2 style="color:#2563eb;margin:0 0 4px">İhaleist</h2>
|
||||||
|
<p style="color:#586074;font-size:14px;margin:0 0 16px">Merhaba ${escapeHtml(name || '')}, takip kriterlerinize uyan ihaleler:</p>
|
||||||
|
<table style="border-collapse:collapse;width:100%;border:1px solid #eef1f5;border-radius:8px;overflow:hidden">
|
||||||
|
<thead><tr style="background:#f6f8fb">
|
||||||
|
<th style="padding:8px 12px;text-align:left;font-size:11px;color:#586074">İhale</th>
|
||||||
|
<th style="padding:8px 12px;text-align:left;font-size:11px;color:#586074">İl</th>
|
||||||
|
<th style="padding:8px 12px;text-align:left;font-size:11px;color:#586074">Tarih</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>${rows || '<tr><td style="padding:12px;color:#8b93a4;font-size:13px" colspan="3">Yeni ihale yok.</td></tr>'}</tbody>
|
||||||
|
</table>
|
||||||
|
<p style="color:#8b93a4;font-size:11px;margin-top:16px">Bu e-posta İhaleist tarafından gönderildi. Bildirim tercihlerinizi uygulama ayarlarından değiştirebilirsiniz.</p>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(s) {
|
||||||
|
return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import path from 'node:path';
|
|||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { readDtTenders } from './dt-store.mjs';
|
import { readDtTenders } from './dt-store.mjs';
|
||||||
import { loginWithGoogle, userFromAuthHeader } from './auth.mjs';
|
import { loginWithGoogle, userFromAuthHeader } from './auth.mjs';
|
||||||
|
import { emailConfigured, sendEmail, tenderDigestHtml } from './email.mjs';
|
||||||
|
|
||||||
// ilan.gov.tr sunucusu ara sertifikayı (GeoTrust TLS RSA CA G1) göndermiyor;
|
// ilan.gov.tr sunucusu ara sertifikayı (GeoTrust TLS RSA CA G1) göndermiyor;
|
||||||
// zinciri tamamlamak için PEM'i yerelden ekliyoruz.
|
// zinciri tamamlamak için PEM'i yerelden ekliyoruz.
|
||||||
@@ -336,6 +337,29 @@ const server = http.createServer(async (req, res) => {
|
|||||||
return json(res, 200, { success: true, user });
|
return json(res, 200, { success: true, user });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- E-posta bildirimi (Resend) ---
|
||||||
|
if (url.pathname === '/api/notify/email/config') {
|
||||||
|
return json(res, 200, { configured: emailConfigured() });
|
||||||
|
}
|
||||||
|
// Giriş yapan kullanıcının KENDİ e-postasına test/özet gönderir (kötüye kullanımı önler).
|
||||||
|
if (url.pathname === '/api/notify/email/test' && req.method === 'POST') {
|
||||||
|
const user = userFromAuthHeader(req.headers['authorization']);
|
||||||
|
if (!user) return json(res, 401, { success: false, error: 'oturum yok' });
|
||||||
|
if (!emailConfigured()) return json(res, 503, { success: false, error: 'E-posta yapılandırılmamış (RESEND_API_KEY yok)' });
|
||||||
|
try {
|
||||||
|
const body = await readBody(req).catch(() => ({}));
|
||||||
|
const html = tenderDigestHtml({ name: user.name || user.email, tenders: body.tenders || [] });
|
||||||
|
const { id } = await sendEmail({
|
||||||
|
to: user.email,
|
||||||
|
subject: body.subject || 'İhaleist — bildirim testi',
|
||||||
|
html,
|
||||||
|
});
|
||||||
|
return json(res, 200, { success: true, id, to: user.email });
|
||||||
|
} catch (e) {
|
||||||
|
return json(res, 502, { success: false, error: e.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (url.pathname === '/api/tenders') {
|
if (url.pathname === '/api/tenders') {
|
||||||
const force = url.searchParams.get('refresh') === '1';
|
const force = url.searchParams.get('refresh') === '1';
|
||||||
const c = await getTenders(force);
|
const c = await getTenders(force);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useApp } from '../context/AppContext';
|
import { useApp } from '../context/AppContext';
|
||||||
import { SECTORS_LIST, CITIES_LIST, INSTITUTION_TYPES_LIST } from '../data/mockTenders';
|
import { SECTORS_LIST, CITIES_LIST, INSTITUTION_TYPES_LIST } from '../data/constants';
|
||||||
import { SlidersHorizontal, Search, RotateCcw, X } from 'lucide-react';
|
import { SlidersHorizontal, Search, RotateCcw, X } from 'lucide-react';
|
||||||
|
|
||||||
export const DetailedSearchModal = () => {
|
export const DetailedSearchModal = () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useApp } from '../context/AppContext';
|
import { useApp } from '../context/AppContext';
|
||||||
import { SECTORS_LIST, INSTITUTION_TYPES_LIST, CITIES_LIST } from '../data/mockTenders';
|
import { SECTORS_LIST, INSTITUTION_TYPES_LIST, CITIES_LIST } from '../data/constants';
|
||||||
import {
|
import {
|
||||||
Compass, Zap, Wrench, Calendar, Eye, EyeOff, FileText, CheckCircle2, XCircle, Archive,
|
Compass, Zap, Wrench, Calendar, Eye, EyeOff, FileText, CheckCircle2, XCircle, Archive,
|
||||||
Bot, PlusCircle, Trash2, ChevronDown, ChevronUp, Filter, MapPin, Building2, Tag, Star
|
Bot, PlusCircle, Trash2, ChevronDown, ChevronUp, Filter, MapPin, Building2, Tag, Star
|
||||||
|
|||||||
@@ -1,42 +1,39 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useApp } from '../context/AppContext';
|
import { useApp } from '../context/AppContext';
|
||||||
import { Database, PlusCircle, Eye } from 'lucide-react';
|
import { Database, PlusCircle, Eye, Layers } from 'lucide-react';
|
||||||
|
|
||||||
export const LiveStatsBar = () => {
|
export const LiveStatsBar = () => {
|
||||||
const { liveStats } = useApp();
|
const { liveStats, tenders } = useApp();
|
||||||
|
|
||||||
|
const yukluIhale = tenders.length;
|
||||||
|
const takibimdeki = tenders.filter((t) => t.takipte).length;
|
||||||
|
const kalemSayisi = tenders.reduce((s, t) => s + (t.items?.length || 0), 0);
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{ deger: liveStats.toplamIhale, etiket: 'Canlı İhale Havuzu', renk: 'text-white', dot: true },
|
||||||
|
{ deger: yukluIhale, etiket: 'Yüklü İhale', renk: 'text-white', icon: Database, iconRenk: 'text-blue-400' },
|
||||||
|
{ deger: liveStats.bugunEklenen, etiket: 'Bugün Eklenen', renk: 'text-emerald-400', icon: PlusCircle, iconRenk: 'text-emerald-400' },
|
||||||
|
{ deger: kalemSayisi, etiket: 'Kalem', renk: 'text-white', icon: Layers, iconRenk: 'text-indigo-400' },
|
||||||
|
{ deger: takibimdeki, etiket: 'Takibimdeki', renk: 'text-white', icon: Eye, iconRenk: 'text-amber-400' },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#111827] text-slate-300 py-1.5 px-4 text-xs font-medium border-b border-slate-800 shadow-inner">
|
<div className="bg-[#111827] text-slate-300 py-1.5 px-4 text-xs font-medium border-b border-slate-800 shadow-inner">
|
||||||
<div className="max-w-[1600px] mx-auto flex items-center justify-center gap-6 sm:gap-10 flex-wrap">
|
<div className="max-w-[1600px] mx-auto flex items-center justify-center gap-6 sm:gap-10 flex-wrap">
|
||||||
|
{stats.map((s) => {
|
||||||
{/* Online Kullanıcı */}
|
const Icon = s.icon;
|
||||||
<div className="flex items-center gap-1.5">
|
return (
|
||||||
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
|
<div key={s.etiket} className="flex items-center gap-1.5">
|
||||||
<span className="font-bold text-white tracking-wide">{liveStats.onlineKullanici.toLocaleString('tr-TR')}</span>
|
{s.dot ? (
|
||||||
<span className="text-slate-400">Online Kullanıcı</span>
|
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
|
||||||
</div>
|
) : (
|
||||||
|
<Icon className={`w-3.5 h-3.5 ${s.iconRenk}`} />
|
||||||
{/* Toplam Kalem */}
|
)}
|
||||||
<div className="flex items-center gap-1.5">
|
<span className={`font-bold ${s.renk} tracking-wide`}>{(s.deger || 0).toLocaleString('tr-TR')}</span>
|
||||||
<Database className="w-3.5 h-3.5 text-blue-400" />
|
<span className="text-slate-400">{s.etiket}</span>
|
||||||
<span className="font-bold text-white tracking-wide">{liveStats.toplamKalem.toLocaleString('tr-TR')}</span>
|
</div>
|
||||||
<span className="text-slate-400">Toplam Kalem</span>
|
);
|
||||||
</div>
|
})}
|
||||||
|
|
||||||
{/* Bugün Eklenen */}
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<PlusCircle className="w-3.5 h-3.5 text-emerald-400" />
|
|
||||||
<span className="font-bold text-emerald-400 tracking-wide">{liveStats.bugunEklenen.toLocaleString('tr-TR')}</span>
|
|
||||||
<span className="text-slate-400">Bugün Eklenen</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Aktif Takip */}
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<Eye className="w-3.5 h-3.5 text-amber-400" />
|
|
||||||
<span className="font-bold text-white tracking-wide">{liveStats.aktifTakip.toLocaleString('tr-TR')}</span>
|
|
||||||
<span className="text-slate-400">Aktif Takip</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useApp } from '../context/AppContext';
|
import { useApp } from '../context/AppContext';
|
||||||
import { MOCK_COMPANY_STATS } from '../data/mockTenders';
|
import { firmStats, companyStats } from '../services/analytics';
|
||||||
import { firmStats } from '../services/analytics';
|
|
||||||
import { Briefcase, FileText, Calendar, History, Trophy, Eye, Kanban, Radar, ArrowRight, Users, Plus, Trash2 } from 'lucide-react';
|
import { Briefcase, FileText, Calendar, History, Trophy, Eye, Kanban, Radar, ArrowRight, Users, Plus, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
export const RightSidebar = () => {
|
export const RightSidebar = () => {
|
||||||
@@ -10,7 +9,7 @@ export const RightSidebar = () => {
|
|||||||
const [tmEmail, setTmEmail] = useState('');
|
const [tmEmail, setTmEmail] = useState('');
|
||||||
const [tmRole, setTmRole] = useState('Uzman');
|
const [tmRole, setTmRole] = useState('Uzman');
|
||||||
|
|
||||||
const trackedCount = tenders.filter(t => t.takipte).length;
|
const cs = companyStats(tenders);
|
||||||
const topFirms = firmStats(tenders).firms.slice(0, 3);
|
const topFirms = firmStats(tenders).firms.slice(0, 3);
|
||||||
|
|
||||||
const handleAddMember = (e) => {
|
const handleAddMember = (e) => {
|
||||||
@@ -21,12 +20,10 @@ export const RightSidebar = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const records = [
|
const records = [
|
||||||
{ label: 'Notlarım', count: MOCK_COMPANY_STATS.notlarimCount, icon: FileText, filterKey: 'all' },
|
{ label: 'İncelediklerim', count: cs.reviewed, icon: History, filterKey: 'incelenenler' },
|
||||||
{ label: 'Ajanda', count: MOCK_COMPANY_STATS.ajandaCount, icon: Calendar, filterKey: 'all' },
|
{ label: 'Sözleşmelerim', count: cs.contracts, icon: FileText, filterKey: 'sozlesmeler' },
|
||||||
{ label: 'İncelediklerim', count: MOCK_COMPANY_STATS.incelediklerimCount, icon: History, filterKey: 'incelenenler' },
|
{ label: 'Sonuçlarım', count: cs.resulted, icon: Trophy, filterKey: 'sonuclananlar' },
|
||||||
{ label: 'Sözleşmelerim', count: MOCK_COMPANY_STATS.sozlesmelerimCount, icon: FileText, filterKey: 'sozlesmeler' },
|
{ label: 'Takibimdekiler', count: cs.tracked, icon: Eye, filterKey: 'takiptekiler' }
|
||||||
{ label: 'Sonuçlarım', count: MOCK_COMPANY_STATS.sonuclarimCount, icon: Trophy, filterKey: 'sonuclananlar' },
|
|
||||||
{ label: 'Takibimdekiler', count: trackedCount, icon: Eye, filterKey: 'takiptekiler' }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useApp } from '../context/AppContext';
|
import { useApp } from '../context/AppContext';
|
||||||
import { INSTITUTION_TYPES_LIST, CITIES_LIST, SECTORS_LIST } from '../data/mockTenders';
|
import { INSTITUTION_TYPES_LIST, CITIES_LIST, SECTORS_LIST } from '../data/constants';
|
||||||
import { Bot, ArrowLeft, CheckCircle2, HelpCircle, Building2, MapPin, Tag } from 'lucide-react';
|
import { Bot, ArrowLeft, CheckCircle2, HelpCircle, Building2, MapPin, Tag } from 'lucide-react';
|
||||||
|
|
||||||
export const SmartFilterBuilder = () => {
|
export const SmartFilterBuilder = () => {
|
||||||
|
|||||||
@@ -1,40 +1,30 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useApp } from '../../context/AppContext';
|
import { useApp } from '../../context/AppContext';
|
||||||
import { BarChart3, TrendingUp, Trophy, Target, MapPin, Building2, Award, ArrowUpRight, DollarSign } from 'lucide-react';
|
import { BarChart3, TrendingUp, Trophy, Target, Award, ArrowUpRight, Pencil } from 'lucide-react';
|
||||||
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, PieChart, Pie, Cell } from 'recharts';
|
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, PieChart, Pie, Cell } from 'recharts';
|
||||||
import { TurkeyHeatMap } from './TurkeyHeatMap';
|
import { TurkeyHeatMap } from './TurkeyHeatMap';
|
||||||
|
import { companyStats, monthlyStats, sectorStats, formatTL } from '../../services/analytics';
|
||||||
|
|
||||||
|
const PIE_COLORS = ['#2563eb', '#059669', '#7c3aed', '#d97706', '#dc2626', '#0891b2', '#db2777', '#65a30d'];
|
||||||
|
|
||||||
export const AnalyticsDashboard = () => {
|
export const AnalyticsDashboard = () => {
|
||||||
const { tenders, userProfile } = useApp();
|
const { tenders, userProfile } = useApp();
|
||||||
|
|
||||||
const monthlyData = [
|
const cs = companyStats(tenders);
|
||||||
{ month: 'Ocak', kazanim: 380000, katilim: 550000 },
|
const monthlyData = monthlyStats(tenders);
|
||||||
{ month: 'Şubat', kazanim: 420000, katilim: 600000 },
|
const sektorler = sectorStats(tenders);
|
||||||
{ month: 'Mart', kazanim: 510000, katilim: 720000 },
|
const pieData = sektorler.slice(0, 6).map((s, i) => ({ name: s.sektor, value: s.count, color: PIE_COLORS[i % PIE_COLORS.length] }));
|
||||||
{ month: 'Nisan', kazanim: 490000, katilim: 680000 },
|
const pieToplam = pieData.reduce((s, p) => s + p.value, 0);
|
||||||
{ month: 'Mayıs', kazanim: 610000, katilim: 850000 },
|
|
||||||
{ month: 'Haziran', kazanim: 580000, katilim: 810000 },
|
|
||||||
{ month: 'Temmuz', kazanim: 410000, katilim: 590000 }
|
|
||||||
];
|
|
||||||
|
|
||||||
const pieData = [
|
// Ciro hedefi — kullanıcı belirler (localStorage)
|
||||||
{ name: 'Tıbbi Malzeme', value: 45, color: '#2563eb' },
|
const [hedef, setHedef] = useState(() => Number(localStorage.getItem('ihaleist_ciro_hedefi') || 0));
|
||||||
{ name: 'Kırtasiye & Büro', value: 25, color: '#059669' },
|
const [editing, setEditing] = useState(false);
|
||||||
{ name: 'Temizlik & Hijyen', value: 18, color: '#7c3aed' },
|
useEffect(() => { localStorage.setItem('ihaleist_ciro_hedefi', String(hedef)); }, [hedef]);
|
||||||
{ name: 'Bilişim & Donanım', value: 12, color: '#d97706' }
|
const hedefYuzde = hedef > 0 ? Math.min(Math.round((cs.wonTotal / hedef) * 100), 100) : 0;
|
||||||
];
|
|
||||||
|
|
||||||
const cityHeatmap = [
|
|
||||||
{ city: 'Ankara', ihaleSayisi: 1420, ortalamaMaliyet: '₺240.000', yogunluk: 'Çok Yüksek (%88)', color: 'bg-red-500 text-white' },
|
|
||||||
{ city: 'Kayseri', ihaleSayisi: 850, ortalamaMaliyet: '₺180.000', yogunluk: 'Yüksek (%74)', color: 'bg-amber-500 text-white' },
|
|
||||||
{ city: 'İstanbul', ihaleSayisi: 2100, ortalamaMaliyet: '₺410.000', yogunluk: 'Çok Yüksek (%94)', color: 'bg-red-600 text-white' },
|
|
||||||
{ city: 'İzmir', ihaleSayisi: 980, ortalamaMaliyet: '₺210.000', yogunluk: 'Orta (%62)', color: 'bg-emerald-500 text-white' },
|
|
||||||
{ city: 'Antalya', ihaleSayisi: 640, ortalamaMaliyet: '₺165.000', yogunluk: 'Orta (%55)', color: 'bg-blue-500 text-white' }
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 animate-fade-in">
|
<div className="space-y-6 animate-fade-in">
|
||||||
|
|
||||||
{/* Header Banner */}
|
{/* Header Banner */}
|
||||||
<div className="bg-linear-to-r from-slate-900 via-purple-950 to-indigo-900 text-white p-6 rounded-2xl shadow-lg border border-purple-800/40 flex items-center justify-between">
|
<div className="bg-linear-to-r from-slate-900 via-purple-950 to-indigo-900 text-white p-6 rounded-2xl shadow-lg border border-purple-800/40 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -43,108 +33,145 @@ export const AnalyticsDashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="font-extrabold text-xl">Analitik Dashboard & Fiyat Isı Haritası</h2>
|
<h2 className="font-extrabold text-xl">Analitik Dashboard & Fiyat Isı Haritası</h2>
|
||||||
<p className="text-xs text-purple-200 mt-0.5">{userProfile.company} firmasına özel kazanma oranı, ciro hedefleri ve il bazlı ihale yoğunluk analizi.</p>
|
<p className="text-xs text-purple-200 mt-0.5">{userProfile.company} — kanban süreçlerinizden hesaplanan gerçek kazanma oranı ve il bazlı ihale yoğunluğu.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* KPI Cards & Ciro Target */}
|
{/* KPI Cards & Ciro Target — gerçek (kanban/takip verisinden) */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
|
||||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-2">
|
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-2">
|
||||||
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider block">Katıldığımız İhaleler</span>
|
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider block">Katıldığımız İhaleler</span>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-2xl font-black text-slate-900">34 İhale</span>
|
<span className="text-2xl font-black text-slate-900">{cs.participated} İhale</span>
|
||||||
<div className="w-8 h-8 rounded-lg bg-blue-50 text-blue-600 flex items-center justify-center">
|
<div className="w-8 h-8 rounded-lg bg-blue-50 text-blue-600 flex items-center justify-center">
|
||||||
<Trophy className="w-4 h-4" />
|
<Trophy className="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<span className="text-[11px] text-slate-400">Teklif verilen + sonuçlanan</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-2">
|
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-2">
|
||||||
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider block">İhale Kazanma Oranımız</span>
|
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider block">İhale Kazanma Oranımız</span>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-2xl font-black text-emerald-600">%68</span>
|
<span className="text-2xl font-black text-emerald-600">%{cs.winRate}</span>
|
||||||
<span className="bg-emerald-100 text-emerald-800 font-extrabold text-[10px] px-2 py-0.5 rounded-full flex items-center gap-0.5">
|
{cs.resulted > 0 && (
|
||||||
<ArrowUpRight className="w-3 h-3" /> +%12
|
<span className="bg-emerald-100 text-emerald-800 font-extrabold text-[10px] px-2 py-0.5 rounded-full flex items-center gap-0.5">
|
||||||
</span>
|
<ArrowUpRight className="w-3 h-3" /> {cs.won}/{cs.resulted}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<span className="text-[11px] text-slate-400">{cs.resulted > 0 ? `${cs.resulted} sonuçlanan ihaleden` : 'Henüz sonuçlanan ihale yok'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-2">
|
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-2">
|
||||||
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider block">Toplam Kazanılan Bedel</span>
|
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider block">Toplam Kazanılan Bedel</span>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-2xl font-black text-indigo-900">₺3.400.000</span>
|
<span className="text-2xl font-black text-indigo-900">{formatTL(cs.wonTotal)}</span>
|
||||||
<div className="w-8 h-8 rounded-lg bg-indigo-50 text-indigo-600 flex items-center justify-center">
|
<div className="w-8 h-8 rounded-lg bg-indigo-50 text-indigo-600 flex items-center justify-center">
|
||||||
<Award className="w-4 h-4" />
|
<Award className="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<span className="text-[11px] text-slate-400">{cs.contracts} sözleşme</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Target Progress Card */}
|
{/* Ciro Hedefi — kullanıcı belirler */}
|
||||||
<div className="bg-linear-to-br from-blue-900 to-indigo-900 text-white p-5 rounded-2xl shadow-xs space-y-2">
|
<div className="bg-linear-to-br from-blue-900 to-indigo-900 text-white p-5 rounded-2xl shadow-xs space-y-2">
|
||||||
<div className="flex justify-between items-center text-xs font-bold">
|
<div className="flex justify-between items-center text-xs font-bold">
|
||||||
<span className="text-blue-200 uppercase tracking-wider flex items-center gap-1">
|
<span className="text-blue-200 uppercase tracking-wider flex items-center gap-1">
|
||||||
<Target className="w-3.5 h-3.5 text-amber-300" /> 2026 Ciro Hedefi
|
<Target className="w-3.5 h-3.5 text-amber-300" /> Ciro Hedefi
|
||||||
</span>
|
</span>
|
||||||
<span className="text-amber-300 font-black">%68</span>
|
<button onClick={() => setEditing((e) => !e)} className="text-blue-200 hover:text-white" title="Hedefi düzenle">
|
||||||
</div>
|
{hedef > 0 ? <span className="text-amber-300 font-black">%{hedefYuzde}</span> : <Pencil className="w-3.5 h-3.5" />}
|
||||||
<span className="text-xl font-black text-white block">₺3.4M / ₺5.0M</span>
|
</button>
|
||||||
<div className="w-full bg-blue-950 rounded-full h-2 overflow-hidden border border-blue-700">
|
|
||||||
<div className="bg-amber-400 h-2 rounded-full" style={{ width: '68%' }} />
|
|
||||||
</div>
|
</div>
|
||||||
|
{editing || hedef === 0 ? (
|
||||||
|
<div className="flex items-center gap-1.5 pt-1">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={hedef || ''}
|
||||||
|
onChange={(e) => setHedef(Number(e.target.value) || 0)}
|
||||||
|
placeholder="Hedef bedel (₺)"
|
||||||
|
className="w-full bg-blue-950/60 border border-blue-700 rounded-lg px-2 py-1.5 text-xs text-white placeholder:text-blue-300"
|
||||||
|
/>
|
||||||
|
<button onClick={() => setEditing(false)} className="bg-amber-400 text-blue-950 font-black text-xs px-2.5 py-1.5 rounded-lg">Kaydet</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="text-lg font-black text-white block">{formatTL(cs.wonTotal)} / {formatTL(hedef)}</span>
|
||||||
|
<div className="w-full bg-blue-950 rounded-full h-2 overflow-hidden border border-blue-700">
|
||||||
|
<div className="bg-amber-400 h-2 rounded-full transition-all" style={{ width: `${hedefYuzde}%` }} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Recharts Row */}
|
{/* Recharts Row — gerçek veriden */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
|
||||||
{/* Monthly Revenue Chart */}
|
{/* Monthly Revenue Chart */}
|
||||||
<div className="lg:col-span-2 bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-4">
|
<div className="lg:col-span-2 bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-4">
|
||||||
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
|
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
|
||||||
<TrendingUp className="w-4 h-4 text-blue-600" /> Aylık İhale Kazanım ve Teklif Hacmi (TL)
|
<TrendingUp className="w-4 h-4 text-blue-600" /> Aylık İhale Kazanım ve Teklif Hacmi (TL)
|
||||||
</h3>
|
</h3>
|
||||||
<div className="h-64 w-full">
|
<div className="h-64 w-full">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
{monthlyData.length === 0 ? (
|
||||||
<BarChart data={monthlyData}>
|
<div className="h-full flex flex-col items-center justify-center text-center border-2 border-dashed border-slate-200 rounded-xl">
|
||||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
|
<p className="text-sm font-bold text-slate-400">Henüz teklif/sonuç verisi yok</p>
|
||||||
<XAxis dataKey="month" tick={{ fontSize: 11 }} />
|
<p className="text-xs text-slate-400 mt-1 max-w-xs">Kanban'da ihaleleri "teklif verildi / kazanıldı / kaybedildi" durumuna aldıkça aylık hacim grafiği oluşur.</p>
|
||||||
<YAxis tick={{ fontSize: 10 }} width={60} tickFormatter={(val) => `₺${val/1000}k`} />
|
</div>
|
||||||
<Tooltip formatter={(value) => [`₺${value.toLocaleString('tr-TR')}`, '']} />
|
) : (
|
||||||
<Bar dataKey="katilim" fill="#94a3b8" name="Toplam Verilen Teklif" radius={[4, 4, 0, 0]} />
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<Bar dataKey="kazanim" fill="#2563eb" name="Kazanılan İhale Hacmi" radius={[4, 4, 0, 0]} />
|
<BarChart data={monthlyData}>
|
||||||
</BarChart>
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
|
||||||
</ResponsiveContainer>
|
<XAxis dataKey="month" tick={{ fontSize: 11 }} />
|
||||||
|
<YAxis tick={{ fontSize: 10 }} width={60} tickFormatter={(val) => `₺${Math.round(val / 1000)}k`} />
|
||||||
|
<Tooltip formatter={(value) => [`₺${value.toLocaleString('tr-TR')}`, '']} />
|
||||||
|
<Bar dataKey="katilim" fill="#94a3b8" name="Toplam Verilen Teklif" radius={[4, 4, 0, 0]} />
|
||||||
|
<Bar dataKey="kazanim" fill="#2563eb" name="Kazanılan İhale Hacmi" radius={[4, 4, 0, 0]} />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sector Distribution Pie */}
|
{/* Sector Distribution Pie */}
|
||||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-4">
|
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-4">
|
||||||
<h3 className="font-extrabold text-sm text-slate-900">Sektörel İhale Kazanım Dağılımı</h3>
|
<h3 className="font-extrabold text-sm text-slate-900">Sektörel İhale Dağılımı</h3>
|
||||||
<div className="h-48 w-full">
|
{pieData.length === 0 ? (
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<div className="h-48 flex items-center justify-center text-center border-2 border-dashed border-slate-200 rounded-xl">
|
||||||
<PieChart>
|
<p className="text-xs text-slate-400 max-w-[180px]">Sektör dağılımı, ihale verisi yüklendikçe hesaplanır.</p>
|
||||||
<Pie data={pieData} dataKey="value" nameKey="name" cx="50%" cy="50%" innerRadius={40} outerRadius={70} paddingAngle={4}>
|
</div>
|
||||||
{pieData.map((entry, index) => (
|
) : (
|
||||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
<>
|
||||||
))}
|
<div className="h-48 w-full">
|
||||||
</Pie>
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<Tooltip formatter={(value) => [`%${value}`, 'Oran']} />
|
<PieChart>
|
||||||
</PieChart>
|
<Pie data={pieData} dataKey="value" nameKey="name" cx="50%" cy="50%" innerRadius={40} outerRadius={70} paddingAngle={4}>
|
||||||
</ResponsiveContainer>
|
{pieData.map((entry, index) => (
|
||||||
</div>
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
<div className="space-y-1.5 pt-2">
|
))}
|
||||||
{pieData.map(item => (
|
</Pie>
|
||||||
<div key={item.name} className="flex justify-between items-center text-xs font-semibold">
|
<Tooltip formatter={(value) => [`${value} ihale`, 'Adet']} />
|
||||||
<span className="flex items-center gap-2">
|
</PieChart>
|
||||||
<span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: item.color }}></span>
|
</ResponsiveContainer>
|
||||||
<span>{item.name}</span>
|
|
||||||
</span>
|
|
||||||
<span className="font-bold text-slate-900">%{item.value}</span>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
<div className="space-y-1.5 pt-2">
|
||||||
</div>
|
{pieData.map((item) => (
|
||||||
|
<div key={item.name} className="flex justify-between items-center text-xs font-semibold">
|
||||||
|
<span className="flex items-center gap-2 min-w-0">
|
||||||
|
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: item.color }}></span>
|
||||||
|
<span className="truncate">{item.name}</span>
|
||||||
|
</span>
|
||||||
|
<span className="font-bold text-slate-900 shrink-0">%{pieToplam ? Math.round((item.value / pieToplam) * 100) : 0}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { COMPETITOR_DATA } from '../../data/mockTenders';
|
import { useApp } from '../../context/AppContext';
|
||||||
import { FileCheck2, Search, Building2, TrendingUp, Award, Shield } from 'lucide-react';
|
import { firmStats, formatTL } from '../../services/analytics';
|
||||||
|
import { FileCheck2, Search, Award } from 'lucide-react';
|
||||||
|
|
||||||
export const ContractSearch = () => {
|
export const ContractSearch = () => {
|
||||||
|
const { tenders } = useApp();
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
const filteredCompetitors = COMPETITOR_DATA.filter(c =>
|
const { firms } = firmStats(tenders);
|
||||||
c.name.toLowerCase().includes(searchTerm.toLowerCase())
|
const filtered = firms.filter((c) =>
|
||||||
|
c.name.toLocaleLowerCase('tr-TR').includes(searchTerm.toLocaleLowerCase('tr-TR'))
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 my-4 animate-fade-in">
|
<div className="space-y-4 my-4 animate-fade-in">
|
||||||
|
|
||||||
<div className="bg-white rounded-xl border border-slate-200 p-5 shadow-xs flex flex-col md:flex-row items-center justify-between gap-4">
|
<div className="bg-white rounded-xl border border-slate-200 p-5 shadow-xs flex flex-col md:flex-row items-center justify-between gap-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 rounded-xl bg-indigo-600 text-white flex items-center justify-center shadow-md">
|
<div className="w-10 h-10 rounded-xl bg-indigo-600 text-white flex items-center justify-center shadow-md">
|
||||||
@@ -19,7 +22,7 @@ export const ContractSearch = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-black text-slate-900">Sözleşme Bedeli & Rakip Analizi</h1>
|
<h1 className="text-xl font-black text-slate-900">Sözleşme Bedeli & Rakip Analizi</h1>
|
||||||
<p className="text-xs text-slate-500">Rakip firmaların kamu ihalelerinden aldığı toplam iş hacmini ve sözleşme bedellerini sorgulayın.</p>
|
<p className="text-xs text-slate-500">Kazanan firmaların sözleşme hacmini ve pazar payını canlı veriden sorgulayın.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -35,43 +38,53 @@ export const ContractSearch = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Competitor Cards */}
|
{filtered.length === 0 ? (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="bg-white rounded-xl border border-dashed border-slate-300 p-10 text-center">
|
||||||
{filteredCompetitors.map((comp, idx) => (
|
<Award className="w-8 h-8 text-slate-300 mx-auto mb-3" />
|
||||||
<div key={idx} className="bg-white rounded-xl border border-slate-200 p-5 shadow-xs hover:shadow-md transition-all space-y-4">
|
<p className="text-sm font-bold text-slate-500">Henüz kazanan firma verisi yok</p>
|
||||||
<div className="flex items-start justify-between border-b border-slate-100 pb-3">
|
<p className="text-xs text-slate-400 mt-1 max-w-md mx-auto leading-relaxed">
|
||||||
|
Kazanan firma ve sözleşme bedeli verisi, doğrudan temin sonuçları ve EKAP sonuç ilanları
|
||||||
|
biriktikçe otomatik olarak burada listelenir.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{filtered.map((comp, idx) => (
|
||||||
|
<div key={idx} className="bg-white rounded-xl border border-slate-200 p-5 shadow-xs hover:shadow-md transition-all space-y-4">
|
||||||
|
<div className="flex items-start justify-between border-b border-slate-100 pb-3">
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-extrabold bg-indigo-100 text-indigo-800 px-2 py-0.5 rounded">
|
||||||
|
Sıra #{idx + 1}
|
||||||
|
</span>
|
||||||
|
<h3 className="font-extrabold text-base text-slate-900 mt-1">{comp.name}</h3>
|
||||||
|
</div>
|
||||||
|
<Award className="w-6 h-6 text-indigo-600 shrink-0" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3 text-xs">
|
||||||
|
<div className="bg-slate-50 p-3 rounded-lg border border-slate-200">
|
||||||
|
<span className="text-slate-400 block font-medium">Toplam Sözleşme</span>
|
||||||
|
<span className="text-sm font-black text-indigo-900 mt-0.5 block">{formatTL(comp.total)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-50 p-3 rounded-lg border border-slate-200">
|
||||||
|
<span className="text-slate-400 block font-medium">Kazandığı İhale</span>
|
||||||
|
<span className="text-sm font-black text-emerald-700 mt-0.5 block">{comp.wins} İhale</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-extrabold bg-indigo-100 text-indigo-800 px-2 py-0.5 rounded">
|
<div className="flex justify-between text-xs font-semibold text-slate-700 mb-1">
|
||||||
Sektör Sırası #{idx + 1}
|
<span>Pazar Payı (hacim)</span>
|
||||||
</span>
|
<span className="font-bold text-indigo-700">%{comp.share}</span>
|
||||||
<h3 className="font-extrabold text-base text-slate-900 mt-1">{comp.name}</h3>
|
</div>
|
||||||
</div>
|
<div className="w-full bg-slate-100 rounded-full h-2 overflow-hidden">
|
||||||
<Award className="w-6 h-6 text-indigo-600 shrink-0" />
|
<div className="bg-indigo-600 h-2 rounded-full" style={{ width: `${Math.max(comp.share, 2)}%` }}></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3 text-xs">
|
|
||||||
<div className="bg-slate-50 p-3 rounded-lg border border-slate-200">
|
|
||||||
<span className="text-slate-400 block font-medium">Toplam İş Hacmi</span>
|
|
||||||
<span className="text-sm font-black text-indigo-900 mt-0.5 block">{comp.totalVolume}</span>
|
|
||||||
</div>
|
|
||||||
<div className="bg-slate-50 p-3 rounded-lg border border-slate-200">
|
|
||||||
<span className="text-slate-400 block font-medium">Kazandığı İhale</span>
|
|
||||||
<span className="text-sm font-black text-emerald-700 mt-0.5 block">{comp.winCount} Adet İhale</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
<div>
|
</div>
|
||||||
<div className="flex justify-between text-xs font-semibold text-slate-700 mb-1">
|
)}
|
||||||
<span>Tahmini Pazar Payı</span>
|
|
||||||
<span className="font-bold text-indigo-700">%{comp.share}</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-slate-100 rounded-full h-2 overflow-hidden">
|
|
||||||
<div className="bg-indigo-600 h-2 rounded-full" style={{ width: `${comp.share}%` }}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,50 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useApp } from '../../context/AppContext';
|
import { useApp } from '../../context/AppContext';
|
||||||
import { Smartphone, Wifi, WifiOff, Download, Bell, CheckCircle2, ShieldCheck, Zap, Sparkles, Moon } from 'lucide-react';
|
import { Smartphone, Wifi, WifiOff, Download, Bell, CheckCircle2, Moon, Mail, Send } from 'lucide-react';
|
||||||
import confetti from 'canvas-confetti';
|
import confetti from 'canvas-confetti';
|
||||||
|
import { getToken } from '../../services/auth';
|
||||||
|
|
||||||
export const PwaSettings = () => {
|
export const PwaSettings = () => {
|
||||||
const { userProfile, reminderSettings, setReminderSettings, requestPushPermission, notify, isQuietNow } = useApp();
|
const { userProfile, authUser, tenders, reminderSettings, setReminderSettings, requestPushPermission, notify, isQuietNow } = useApp();
|
||||||
const [offlineMode, setOfflineMode] = useState(true);
|
const [offlineMode, setOfflineMode] = useState(true);
|
||||||
const [installed, setInstalled] = useState(false);
|
const [installed, setInstalled] = useState(false);
|
||||||
const [permission, setPermission] = useState(typeof Notification !== 'undefined' ? Notification.permission : 'unsupported');
|
const [permission, setPermission] = useState(typeof Notification !== 'undefined' ? Notification.permission : 'unsupported');
|
||||||
|
|
||||||
|
// E-posta (Resend) durumu + test gönderimi
|
||||||
|
const [emailCfg, setEmailCfg] = useState(null); // null=bilinmiyor, true/false
|
||||||
|
const [emailSending, setEmailSending] = useState(false);
|
||||||
|
const [emailResult, setEmailResult] = useState(null);
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/notify/email/config')
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((d) => setEmailCfg(Boolean(d.configured)))
|
||||||
|
.catch(() => setEmailCfg(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const sendTestEmail = async () => {
|
||||||
|
setEmailSending(true);
|
||||||
|
setEmailResult(null);
|
||||||
|
try {
|
||||||
|
const yeniler = tenders.filter((t) => t.isYeni).slice(0, 15);
|
||||||
|
const res = await fetch('/api/notify/email/test', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` },
|
||||||
|
body: JSON.stringify({ subject: 'İhaleist — bildirim testi', tenders: yeniler }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
setEmailResult({ ok: true, msg: `E-posta gönderildi → ${data.to}` });
|
||||||
|
notify('E-posta gönderildi', `Test e-postası ${data.to} adresine iletildi.`);
|
||||||
|
} else {
|
||||||
|
setEmailResult({ ok: false, msg: data.error || 'Gönderilemedi' });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setEmailResult({ ok: false, msg: e.message });
|
||||||
|
} finally {
|
||||||
|
setEmailSending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleEnablePush = async () => {
|
const handleEnablePush = async () => {
|
||||||
const p = await requestPushPermission();
|
const p = await requestPushPermission();
|
||||||
setPermission(p);
|
setPermission(p);
|
||||||
@@ -134,6 +170,49 @@ export const PwaSettings = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* E-posta Bildirimi (Resend) — gerçek gönderim */}
|
||||||
|
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-xs space-y-4">
|
||||||
|
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
|
||||||
|
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
|
||||||
|
<Mail className="w-4 h-4 text-blue-500" /> E-posta Bildirimi
|
||||||
|
</h3>
|
||||||
|
<span className={`font-extrabold text-[10px] px-2 py-0.5 rounded-full ${
|
||||||
|
emailCfg === true ? 'bg-emerald-100 text-emerald-800' : emailCfg === false ? 'bg-amber-100 text-amber-800' : 'bg-slate-100 text-slate-600'
|
||||||
|
}`}>
|
||||||
|
{emailCfg === true ? 'Aktif (Resend)' : emailCfg === false ? 'Yapılandırılmadı' : 'Kontrol ediliyor…'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-slate-600 leading-relaxed">
|
||||||
|
{emailCfg === false
|
||||||
|
? 'E-posta göndermek için sunucuya RESEND_API_KEY ekleyin (Coolify → Environment). Ayrıca gönderen domainini Resend’de DNS ile doğrulayın.'
|
||||||
|
: 'Takip kriterlerinize uyan ihaleler, hesabınızın e-posta adresine özet olarak gönderilir. Aşağıdan kendinize test gönderebilirsiniz.'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<label className="flex items-center justify-between text-xs font-bold text-slate-800 cursor-pointer">
|
||||||
|
<span>E-posta bildirimleri açık</span>
|
||||||
|
<input type="checkbox" checked={reminderSettings.emailEnabled}
|
||||||
|
onChange={(e) => setReminderSettings((s) => ({ ...s, emailEnabled: e.target.checked }))}
|
||||||
|
className="w-4 h-4 accent-blue-600 rounded" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={sendTestEmail}
|
||||||
|
disabled={emailSending || emailCfg !== true || !authUser}
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white font-bold text-xs py-2.5 rounded-xl flex items-center justify-center gap-2"
|
||||||
|
title={!authUser ? 'Giriş yapın' : emailCfg !== true ? 'RESEND_API_KEY gerekli' : 'Kendinize test e-postası gönder'}
|
||||||
|
>
|
||||||
|
<Send className="w-3.5 h-3.5" />
|
||||||
|
{emailSending ? 'Gönderiliyor…' : 'Kendime Test E-postası Gönder'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{emailResult && (
|
||||||
|
<p className={`text-[11px] ${emailResult.ok ? 'text-emerald-600' : 'text-red-600'}`}>
|
||||||
|
{emailResult.ok ? '✓ ' : '⚠ '}{emailResult.msg}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Install PWA Application */}
|
{/* Install PWA Application */}
|
||||||
<div className="bg-linear-to-br from-slate-900 to-cyan-950 text-white p-6 rounded-2xl shadow-xs space-y-4 border border-cyan-800/40">
|
<div className="bg-linear-to-br from-slate-900 to-cyan-950 text-white p-6 rounded-2xl shadow-xs space-y-4 border border-cyan-800/40">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
+32
-61
@@ -1,5 +1,4 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||||
import { MOCK_TENDERS, MOCK_LIVE_STATS, MOCK_COMPANY_STATS } from '../data/mockTenders';
|
|
||||||
import { parseWebsiteCapabilitiesDynamically } from '../services/webScraperService';
|
import { parseWebsiteCapabilitiesDynamically } from '../services/webScraperService';
|
||||||
import { fetchLiveTenders, fetchTenderDetail, mergeUserState } from '../services/tenderApi';
|
import { fetchLiveTenders, fetchTenderDetail, mergeUserState } from '../services/tenderApi';
|
||||||
import { restoreSession, clearSession, getStoredUser } from '../services/auth';
|
import { restoreSession, clearSession, getStoredUser } from '../services/auth';
|
||||||
@@ -10,10 +9,10 @@ export const AppProvider = ({ children }) => {
|
|||||||
const [activeTab, setActiveTab] = useState('list'); // 'list', 'bulten', 'sonuc', 'takvim', 'sozlesme', 'kdv', 'fiyat', 'kanban', 'smart_filter_builder'
|
const [activeTab, setActiveTab] = useState('list'); // 'list', 'bulten', 'sonuc', 'takvim', 'sozlesme', 'kdv', 'fiyat', 'kanban', 'smart_filter_builder'
|
||||||
const [tenders, setTenders] = useState(() => {
|
const [tenders, setTenders] = useState(() => {
|
||||||
const saved = localStorage.getItem('ihaleist_tenders');
|
const saved = localStorage.getItem('ihaleist_tenders');
|
||||||
return saved ? JSON.parse(saved) : MOCK_TENDERS;
|
return saved ? JSON.parse(saved) : [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const [liveStats, setLiveStats] = useState(MOCK_LIVE_STATS);
|
const [liveStats, setLiveStats] = useState({ toplamIhale: 0, bugunEklenen: 0 });
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [selectedCity, setSelectedCity] = useState('');
|
const [selectedCity, setSelectedCity] = useState('');
|
||||||
const [selectedAlimTuru, setSelectedAlimTuru] = useState('');
|
const [selectedAlimTuru, setSelectedAlimTuru] = useState('');
|
||||||
@@ -24,10 +23,7 @@ export const AppProvider = ({ children }) => {
|
|||||||
|
|
||||||
const [savedFilters, setSavedFilters] = useState(() => {
|
const [savedFilters, setSavedFilters] = useState(() => {
|
||||||
const saved = localStorage.getItem('ihaleist_saved_filters');
|
const saved = localStorage.getItem('ihaleist_saved_filters');
|
||||||
return saved ? JSON.parse(saved) : [
|
return saved ? JSON.parse(saved) : [];
|
||||||
{ id: 'sf-1', name: 'Sağlık Hizmeti İhaleleri', user: 'hikmet', kurumTipi: 'Eğitim ve Araştırma Hastaneleri', sehir: 'Ankara' },
|
|
||||||
{ id: 'sf-2', name: 'Kayseri Kırtasiye Alımları', user: 'hikmet', kurumTipi: 'Ceza İnfaz Kurumları', sehir: 'Kayseri' }
|
|
||||||
];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isLiveMode, setIsLiveMode] = useState(true);
|
const [isLiveMode, setIsLiveMode] = useState(true);
|
||||||
@@ -61,11 +57,10 @@ export const AppProvider = ({ children }) => {
|
|||||||
const data = await fetchLiveTenders({ refresh });
|
const data = await fetchLiveTenders({ refresh });
|
||||||
setTenders(prev => mergeUserState(data.tenders, prev));
|
setTenders(prev => mergeUserState(data.tenders, prev));
|
||||||
setLiveDataSource({ source: data.source, fetchedAt: data.fetchedAt });
|
setLiveDataSource({ source: data.source, fetchedAt: data.fetchedAt });
|
||||||
setLiveStats(prev => ({
|
setLiveStats({
|
||||||
...prev,
|
toplamIhale: data.totalCount ?? 0,
|
||||||
bugunEklenen: data.todayCount ?? prev.bugunEklenen,
|
bugunEklenen: data.todayCount ?? 0,
|
||||||
aktifTakip: data.totalCount ?? prev.aktifTakip
|
});
|
||||||
}));
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setLiveDataError(err.message);
|
setLiveDataError(err.message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -107,11 +102,13 @@ export const AppProvider = ({ children }) => {
|
|||||||
const [aiAnalysisResults, setAiAnalysisResults] = useState(null);
|
const [aiAnalysisResults, setAiAnalysisResults] = useState(null);
|
||||||
|
|
||||||
// Enterprise Module States
|
// Enterprise Module States
|
||||||
const [competitorWatchlist, setCompetitorWatchlist] = useState([
|
// Rakip izleme listesi — kullanıcı gerçek kazanan firma analizinden ekler (localStorage)
|
||||||
{ id: 'cw-1', name: 'Bileşim Tıbbi Cihazlar A.Ş.', sector: 'Medikal & Tıbbi Malzeme', winCount: 24, totalRevenue: 14200000, marketShare: 32, status: 'Aktif İzleniyor', alertOnBid: true, alertOnWin: true },
|
const [competitorWatchlist, setCompetitorWatchlist] = useState(() => {
|
||||||
{ id: 'cw-2', name: 'Atlas Sağlık ve Kırtasiye Ltd.', sector: 'Sağlık & Büro Malzemeleri', winCount: 18, totalRevenue: 9800000, marketShare: 24, status: 'Aktif İzleniyor', alertOnBid: true, alertOnWin: true },
|
try { return JSON.parse(localStorage.getItem('ihaleist_rakip_izleme') || '[]'); } catch { return []; }
|
||||||
{ id: 'cw-3', name: 'Tekno-Med Laboratuvar Sistemleri', sector: 'Laboratuvar & Bilişim', winCount: 12, totalRevenue: 6500000, marketShare: 16, status: 'Aktif İzleniyor', alertOnBid: false, alertOnWin: true }
|
});
|
||||||
]);
|
useEffect(() => {
|
||||||
|
localStorage.setItem('ihaleist_rakip_izleme', JSON.stringify(competitorWatchlist));
|
||||||
|
}, [competitorWatchlist]);
|
||||||
|
|
||||||
// --- Favori kurumlar (localStorage, gerçek) ---
|
// --- Favori kurumlar (localStorage, gerçek) ---
|
||||||
const [favoriteKurumlar, setFavoriteKurumlar] = useState(() => {
|
const [favoriteKurumlar, setFavoriteKurumlar] = useState(() => {
|
||||||
@@ -142,13 +139,13 @@ export const AppProvider = ({ children }) => {
|
|||||||
const addTeamMember = (m) => setTeamMembers((prev) => [...prev, { id: 'tm-' + Date.now(), ...m }]);
|
const addTeamMember = (m) => setTeamMembers((prev) => [...prev, { id: 'tm-' + Date.now(), ...m }]);
|
||||||
const removeTeamMember = (id) => setTeamMembers((prev) => prev.filter((m) => m.id !== id));
|
const removeTeamMember = (id) => setTeamMembers((prev) => prev.filter((m) => m.id !== id));
|
||||||
|
|
||||||
const [vaultDocuments, setVaultDocuments] = useState([
|
// Belge kasası — kullanıcı kendi belgelerini ekler (localStorage)
|
||||||
{ id: 'doc-1', name: 'İmza Sirküleri 2026.pdf', category: 'Yasal Belgeler', size: '2.4 MB', updatedAt: '2026-01-15', expiresAt: '2027-01-15', status: 'Geçerli' },
|
const [vaultDocuments, setVaultDocuments] = useState(() => {
|
||||||
{ id: 'doc-2', name: 'ISO 13485 Tıbbi Cihaz Sertifikası.pdf', category: 'Kalite Belgeleri', size: '1.8 MB', updatedAt: '2025-11-10', expiresAt: '2028-11-10', status: 'Geçerli' },
|
try { return JSON.parse(localStorage.getItem('ihaleist_belgeler') || '[]'); } catch { return []; }
|
||||||
{ id: 'doc-3', name: 'Oda Faaliyet Belgesi.pdf', category: 'Resmi Kayıtlar', size: '1.1 MB', updatedAt: '2026-02-01', expiresAt: '2026-08-01', status: 'Geçerli' },
|
});
|
||||||
{ id: 'doc-4', name: 'ÜTS Firma Kayıt Belgesi.pdf', category: 'Tıbbi Cihaz Kaydı', size: '3.2 MB', updatedAt: '2026-03-20', expiresAt: '2027-03-20', status: 'Geçerli' },
|
useEffect(() => {
|
||||||
{ id: 'doc-5', name: 'Vergi Levhası 2025.pdf', category: 'Mali Belgeler', size: '850 KB', updatedAt: '2025-05-30', expiresAt: '2026-05-30', status: 'Geçerli' }
|
localStorage.setItem('ihaleist_belgeler', JSON.stringify(vaultDocuments));
|
||||||
]);
|
}, [vaultDocuments]);
|
||||||
|
|
||||||
const [reminderSettings, setReminderSettings] = useState(() => {
|
const [reminderSettings, setReminderSettings] = useState(() => {
|
||||||
const def = {
|
const def = {
|
||||||
@@ -178,8 +175,8 @@ export const AppProvider = ({ children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [webhooksConfig, setWebhooksConfig] = useState({
|
const [webhooksConfig, setWebhooksConfig] = useState({
|
||||||
slackWebhookUrl: 'https://hooks.slack.com/services/T00/B00/XXXXXX',
|
slackWebhookUrl: '',
|
||||||
teamsWebhookUrl: 'https://outlook.office.com/webhook/XXXXXX',
|
teamsWebhookUrl: '',
|
||||||
events: {
|
events: {
|
||||||
newTenderMatch: true,
|
newTenderMatch: true,
|
||||||
tenderWon: true,
|
tenderWon: true,
|
||||||
@@ -188,9 +185,7 @@ export const AppProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const [apiKeys, setApiKeys] = useState([
|
const [apiKeys, setApiKeys] = useState([]);
|
||||||
{ id: 'key-1', name: 'ERP Entegrasyon Anahtarı', key: 'ih_live_8f92a19c3b88412e', createdAt: '2026-04-10', lastUsed: 'Bugün 12:45' }
|
|
||||||
]);
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// AI Model Yönetimi (admin panelden seçilebilir + fallback zinciri)
|
// AI Model Yönetimi (admin panelden seçilebilir + fallback zinciri)
|
||||||
@@ -273,23 +268,11 @@ export const AppProvider = ({ children }) => {
|
|||||||
trialDaysLeft: 7,
|
trialDaysLeft: 7,
|
||||||
role: 'Yönetici Tedarikçi',
|
role: 'Yönetici Tedarikçi',
|
||||||
scrapedCapabilities: {
|
scrapedCapabilities: {
|
||||||
scrapedAt: '2026-08-07 14:30',
|
scrapedAt: null,
|
||||||
extractedSectors: ['Tıbbi Cihaz ve Sarf Malzemeleri', 'Sağlık & Laboratuvar Teknolojileri'],
|
extractedSectors: [],
|
||||||
extractedProducts: [
|
extractedProducts: [],
|
||||||
'Ortopedik Cerrahi İmplantlar ve Protezler',
|
extractedCertifications: [],
|
||||||
'Cerrahi Maske ve Steril Önlük Setleri',
|
matchScoreBoost: 0
|
||||||
'Tıbbi Ameliyathane & Yoğun Bakım Karyolaları',
|
|
||||||
'Dezenfektan ve Hastane Sterilizasyon Solüsyonları',
|
|
||||||
'Tek Kullanımlık Enjektör ve İğne Uçları',
|
|
||||||
'Laboratuvar Reaktif ve Biyokimya Test Kitleri'
|
|
||||||
],
|
|
||||||
extractedCertifications: [
|
|
||||||
'ISO 13485 Tıbbi Cihaz Kalite Yönetim Sertifikası',
|
|
||||||
'ÜTS (Ürün Takip Sistemi) Firma Kaydı & Barkod Yetkisi',
|
|
||||||
'CE Tıbbi Cihaz Uygunluk Beyanı',
|
|
||||||
'TSE HYB Belgesi (Medikal Cihaz Bakım-Onarım)'
|
|
||||||
],
|
|
||||||
matchScoreBoost: 20
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -319,10 +302,7 @@ export const AppProvider = ({ children }) => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const [notifications, setNotifications] = useState([
|
const [notifications, setNotifications] = useState([]);
|
||||||
{ id: 'n1', title: 'Yeni İhale Bildirimi', text: 'Sağlık Hizmeti İhaleleri filtrenize 3 yeni ihale eklendi.', time: '5dk önce', read: false },
|
|
||||||
{ id: 'n2', title: 'Yaklaşan İhale Süresi', text: 'KIRTASİYE MALZEMELERİ ALIMI ihaleniz için son 3 gün!', time: '1sa önce', read: false }
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Gerçek bildirim: listeye ekler; sessiz saat dışında ve push açıksa tarayıcı
|
// Gerçek bildirim: listeye ekler; sessiz saat dışında ve push açıksa tarayıcı
|
||||||
// bildirimi (Notification API) da gösterir.
|
// bildirimi (Notification API) da gösterir.
|
||||||
@@ -351,17 +331,8 @@ export const AppProvider = ({ children }) => {
|
|||||||
localStorage.setItem('ihaleist_saved_filters', JSON.stringify(savedFilters));
|
localStorage.setItem('ihaleist_saved_filters', JSON.stringify(savedFilters));
|
||||||
}, [savedFilters]);
|
}, [savedFilters]);
|
||||||
|
|
||||||
// Live counter animation effect
|
// (Sahte "online kullanıcı" rastgele sayacı kaldırıldı — canlı istatistikler
|
||||||
useEffect(() => {
|
// gerçek /api/stats verisinden gelir.)
|
||||||
const interval = setInterval(() => {
|
|
||||||
setLiveStats(prev => ({
|
|
||||||
...prev,
|
|
||||||
onlineKullanici: prev.onlineKullanici + (Math.floor(Math.random() * 5) - 2),
|
|
||||||
bugunEklenen: prev.bugunEklenen + (Math.random() > 0.7 ? 1 : 0)
|
|
||||||
}));
|
|
||||||
}, 4000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const toggleTrackTender = (tenderId) => {
|
const toggleTrackTender = (tenderId) => {
|
||||||
setTenders(prev => prev.map(t => {
|
setTenders(prev => prev.map(t => {
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Sabit referans listeleri (mock veri DEĞİL — açılır menü/filtre seçenekleri).
|
||||||
|
* Sektörler, kurum türleri ve iller Türkiye kamu ihale sisteminin gerçek
|
||||||
|
* kategori listeleridir; il-plaka.mjs gibi kanonik sabitlerdir.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const SECTORS_LIST = [
|
||||||
|
"Sağlık / Tıbbi Malzeme",
|
||||||
|
"Kırtasiye & Büro",
|
||||||
|
"Bilişim & Yazılım",
|
||||||
|
"Gıda & Catering",
|
||||||
|
"İnşaat & Bakım",
|
||||||
|
"Temizlik & Hijyen",
|
||||||
|
"Tekstil & Giyim",
|
||||||
|
"Otomotiv & Yedek Parça",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const INSTITUTION_TYPES_LIST = [
|
||||||
|
"Eğitim ve Araştırma Hastaneleri",
|
||||||
|
"Belediyeler",
|
||||||
|
"Büyükşehir Belediyeleri",
|
||||||
|
"Ceza İnfaz Kurumları",
|
||||||
|
"Üniversiteler",
|
||||||
|
"Bakanlıklar",
|
||||||
|
"Kamu Şirketleri",
|
||||||
|
"Ağız ve Diş Sağlığı Merkezleri",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CITIES_LIST = [
|
||||||
|
"Adana", "Adıyaman", "Afyonkarahisar", "Aksaray", "Amasya", "Ankara", "Antalya", "Ardahan", "Artvin",
|
||||||
|
"Aydın", "Balıkesir", "Bolu", "Bursa", "Çanakkale", "Denizli", "Diyarbakır", "Edirne", "Eskişehir",
|
||||||
|
"Gaziantep", "Giresun", "Isparta", "İstanbul", "İzmir", "Kahramanmaraş", "Kars", "Kastamonu", "Kayseri",
|
||||||
|
"Kocaeli", "Konya", "Kütahya", "Malatya", "Manisa", "Mardin", "Muğla", "Ordu", "Rize", "Sakarya",
|
||||||
|
"Samsun", "Sivas", "Şanlıurfa", "Tekirdağ", "Trabzon", "Van", "Zonguldak",
|
||||||
|
];
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
export const MOCK_TENDERS = [
|
|
||||||
{
|
|
||||||
id: "tender-101",
|
|
||||||
ikn: "26DT1489671",
|
|
||||||
baslik: "KIRTASİYE MALZEMELERİ ALIMI KAYSERİ ÇOCUK CEZAEVİ",
|
|
||||||
kurum: "CEZA VE TEVKİFEVLERİ GENEL MÜDÜRLÜĞÜ KAYSERİ ÇOCUK KAPALI CEZA İNFAZ KURUMU MÜDÜRLÜĞÜ",
|
|
||||||
sehir: "Kayseri",
|
|
||||||
bolge: "İÇ ANADOLU BÖLGESİ",
|
|
||||||
tarih: "10.08.2026",
|
|
||||||
saat: "12:00",
|
|
||||||
alimTuru: "Doğrudan Temin",
|
|
||||||
usul: "Doğrudan Temin (4734 22/d)",
|
|
||||||
sektor: "Kırtasiye & Büro",
|
|
||||||
brans: "Sanatsal Kırtasiye ve El Becerisi Malzemeleri",
|
|
||||||
kurumTipi: "Ceza İnfaz Kurumları",
|
|
||||||
eklenmeZamani: "az önce eklendi",
|
|
||||||
isYeni: true,
|
|
||||||
aciliyetBadge: "⚠️ SON 3 GÜN",
|
|
||||||
items: [
|
|
||||||
{ id: "i1", ad: "35*50 TUVAL", miktar: "10", birim: "adet", birimFiyat: 145 },
|
|
||||||
{ id: "i2", ad: "50*70 TUVAL", miktar: "10", birim: "adet", birimFiyat: 220 },
|
|
||||||
{ id: "i3", ad: "AKRİLİK BOYA SETİ 12'Lİ", miktar: "25", birim: "set", birimFiyat: 185 },
|
|
||||||
{ id: "i4", ad: "RESİM FIRÇASI SETİ NO: 1-12", miktar: "30", birim: "set", birimFiyat: 95 }
|
|
||||||
],
|
|
||||||
aciklama: "Kayseri Çocuk Kapalı Ceza İnfaz Kurumu Müdürlüğü Hobi ve Atölye Çalışmaları İhtiyacı İçin 4 Kalem Kırtasiye ve Sanatsal Malzeme Doğrudan Temin Usulü İle Alınacaktır. Fiyat teklif cetveli kaşeli ve imzalı teslim edilecektir.",
|
|
||||||
yaklasikMaliyet: 18500,
|
|
||||||
teslimatSuresi: "7 Takvim Günü",
|
|
||||||
teslimYeri: "Kayseri Çocuk Cezaevi Deposu",
|
|
||||||
yeterlikKriterleri: ["KDV Hariç Teklif Cetveli", "İmza Sirküleri / Yetki Belgesi", "Faaliyet Belgesi"],
|
|
||||||
aiMatchScore: 94,
|
|
||||||
aiRiskScore: "Düşük",
|
|
||||||
aiBidRange: { min: 14200, max: 17800, recommended: 15600 },
|
|
||||||
kanbanStatus: "teklif_hazirlaniyor",
|
|
||||||
atananPersonel: "Ahmet Yılmaz",
|
|
||||||
durum: "Aktif",
|
|
||||||
incelendi: true,
|
|
||||||
takipte: true,
|
|
||||||
sozlesmeBedeli: null,
|
|
||||||
kazananFirma: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "tender-102",
|
|
||||||
ikn: "26DT1489955",
|
|
||||||
baslik: "Yemek Kabı ve Üst Kapama Filmi Alımı",
|
|
||||||
kurum: "TARIM İŞLETMELERİ GENEL MÜDÜRLÜĞÜ KARACABEY TARIM İŞLETMESİ MÜDÜRLÜĞÜ",
|
|
||||||
sehir: "Bursa",
|
|
||||||
bolge: "MARMARA BÖLGESİ",
|
|
||||||
tarih: "10.08.2026",
|
|
||||||
saat: "18:00",
|
|
||||||
alimTuru: "Doğrudan Temin",
|
|
||||||
usul: "Doğrudan Temin (4734 22/d)",
|
|
||||||
sektor: "Gıda & Catering",
|
|
||||||
brans: "Ambalaj ve Paketleme Ürünleri",
|
|
||||||
kurumTipi: "Kamu Şirketleri",
|
|
||||||
eklenmeZamani: "az önce eklendi",
|
|
||||||
isYeni: true,
|
|
||||||
aciliyetBadge: "⚠️ SON 3 GÜN",
|
|
||||||
items: [
|
|
||||||
{ id: "i1", ad: "1 Gözlü Yemek Kabı", miktar: "1000", birim: "adet", birimFiyat: 2.80 },
|
|
||||||
{ id: "i2", ad: "2 Gözlü Yemek Kabı", miktar: "1000", birim: "adet", birimFiyat: 3.40 },
|
|
||||||
{ id: "i3", ad: "Sızdırmaz Üst Kapama Filmi (Rulo)", miktar: "50", birim: "rulo", birimFiyat: 410 }
|
|
||||||
],
|
|
||||||
aciklama: "Karacabey Tarım İşletmesi personel yemekhanesi için kullanılacak olan ısıya dayanıklı 1 ve 2 gözlü porsiyonluk kaplar ve üst kapatma film ruloları alınacaktır.",
|
|
||||||
yaklasikMaliyet: 26700,
|
|
||||||
teslimatSuresi: "5 İş Günü",
|
|
||||||
teslimYeri: "Karacabey Tarım İşletmesi Ambarı",
|
|
||||||
yeterlikKriterleri: ["Gıda Temas Uygunluk Sertifikası (ISO 22000)", "Teklif Mektubu"],
|
|
||||||
aiMatchScore: 88,
|
|
||||||
aiRiskScore: "Düşük",
|
|
||||||
aiBidRange: { min: 21000, max: 25500, recommended: 23200 },
|
|
||||||
kanbanStatus: "incelenecek",
|
|
||||||
atananPersonel: null,
|
|
||||||
durum: "Aktif",
|
|
||||||
incelendi: false,
|
|
||||||
takipte: false,
|
|
||||||
sozlesmeBedeli: null,
|
|
||||||
kazananFirma: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "tender-103",
|
|
||||||
ikn: "2026/184201",
|
|
||||||
baslik: "193064-PANİK BUTON KURULUM İŞİ",
|
|
||||||
kurum: "ANKARA ÜNİVERSİTESİ REKTÖRLÜĞÜ SAĞLIK UYGULAMA VE ARAŞTIRMA HASTANELERİ BAŞHEKİMLİĞİ",
|
|
||||||
sehir: "Ankara",
|
|
||||||
bolge: "İÇ ANADOLU BÖLGESİ",
|
|
||||||
tarih: "11.08.2026",
|
|
||||||
saat: "10:30",
|
|
||||||
alimTuru: "Doğrudan Temin",
|
|
||||||
usul: "Doğrudan Temin (4734 22/d)",
|
|
||||||
sektor: "Bilişim & Yazılım",
|
|
||||||
brans: "Güvenlik Sistemleri ve Zayıf Akım",
|
|
||||||
kurumTipi: "Üniversiteler",
|
|
||||||
eklenmeZamani: "15 dakika önce",
|
|
||||||
isYeni: true,
|
|
||||||
aciliyetBadge: "🔥 BUGÜN KAPANIYOR",
|
|
||||||
items: [
|
|
||||||
{ id: "i1", ad: "Kablosuz Panik Butonu Verici Ünite", miktar: "15", birim: "adet", birimFiyat: 1250 },
|
|
||||||
{ id: "i2", ad: "Merkezi Alarm Alma Paneli (RF Receiver)", miktar: "2", birim: "adet", birimFiyat: 8400 },
|
|
||||||
{ id: "i3", ad: "Montaj ve Kablolama Hizmeti", miktar: "1", birim: "paket", birimFiyat: 6500 }
|
|
||||||
],
|
|
||||||
aciklama: "İbn-i Sina Araştırma Hastanesi Acil Servis ve Yoğun Bakım üniteleri için acil durum kablosuz panik buton sistemi temini ve devreye alınması işidir.",
|
|
||||||
yaklasikMaliyet: 42000,
|
|
||||||
teslimatSuresi: "10 Gün",
|
|
||||||
teslimYeri: "Ankara Üniversitesi İbn-i Sina Hastanesi",
|
|
||||||
yeterlikKriterleri: ["TSE HYB Belgesi", "Marka Tescil / Yetkili Satıcı Belgesi"],
|
|
||||||
aiMatchScore: 92,
|
|
||||||
aiRiskScore: "Orta",
|
|
||||||
aiBidRange: { min: 34500, max: 40800, recommended: 37200 },
|
|
||||||
kanbanStatus: "inceleniyor",
|
|
||||||
atananPersonel: "Zeynep Kaya",
|
|
||||||
durum: "Aktif",
|
|
||||||
incelendi: true,
|
|
||||||
takipte: true,
|
|
||||||
sozlesmeBedeli: null,
|
|
||||||
kazananFirma: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "tender-104",
|
|
||||||
ikn: "26DT1490112",
|
|
||||||
baslik: "HASTAYA ÖZGÜ ORTOPEDİ MALZEME ALIMI (ZARİFE BALLI)",
|
|
||||||
kurum: "KONYA İL SAĞLIK MÜDÜRLÜĞÜ KONYA AKŞEHİR DEVLET HASTANESİ BAŞHEKİMLİĞİ",
|
|
||||||
sehir: "Konya",
|
|
||||||
bolge: "İÇ ANADOLU BÖLGESİ",
|
|
||||||
tarih: "12.08.2026",
|
|
||||||
saat: "14:00",
|
|
||||||
alimTuru: "Doğrudan Temin",
|
|
||||||
usul: "Doğrudan Temin (4734 22/f)",
|
|
||||||
sektor: "Sağlık / Tıbbi Malzeme",
|
|
||||||
brans: "Ortopedi ve Protez İmplanta Sistemleri",
|
|
||||||
kurumTipi: "Eğitim ve Araştırma Hastaneleri",
|
|
||||||
eklenmeZamani: "1 saat önce",
|
|
||||||
isYeni: false,
|
|
||||||
aciliyetBadge: null,
|
|
||||||
items: [
|
|
||||||
{ id: "i1", ad: "Revizyon Kalça Protez Femoral Stem (Titanyum)", miktar: "1", birim: "set", birimFiyat: 48500 },
|
|
||||||
{ id: "i2", ad: "Asetabular Kap ve Seramik Insert", miktar: "1", birim: "set", birimFiyat: 29000 },
|
|
||||||
{ id: "i3", ad: "Kemik Çimentosu Gentamisinli 40gr", miktar: "2", birim: "kutu", birimFiyat: 3200 }
|
|
||||||
],
|
|
||||||
aciklama: "Hastaya özel doğrudan temin usulü ile kalça revizyon protez seti alımıdır. Ürünlerin ÜTS (Ürün Takip Sistemi) kaydı ve SUT kodları eksiksiz olmalıdır.",
|
|
||||||
yaklasikMaliyet: 83900,
|
|
||||||
teslimatSuresi: "3 Gün (Hasta Ameliyat Tarihi)",
|
|
||||||
teslimYeri: "Akşehir Devlet Hastanesi Ameliyathane Deposu",
|
|
||||||
yeterlikKriterleri: ["ÜTS Barkod Eşleşme Çıktısı", "SUT Bildirimi", "FDA / CE Sertifikası"],
|
|
||||||
aiMatchScore: 96,
|
|
||||||
aiRiskScore: "Düşük",
|
|
||||||
aiBidRange: { min: 72000, max: 81000, recommended: 76500 },
|
|
||||||
kanbanStatus: "teklif_verildi",
|
|
||||||
atananPersonel: "Hikmet Tanrıverdi",
|
|
||||||
durum: "Aktif",
|
|
||||||
incelendi: true,
|
|
||||||
takipte: true,
|
|
||||||
sozlesmeBedeli: null,
|
|
||||||
kazananFirma: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "tender-105",
|
|
||||||
ikn: "2026/178920",
|
|
||||||
baslik: "Klima Santrali Filtre Değişimi ve Bakım Hizmet Alımı",
|
|
||||||
kurum: "İZMİR BÜYÜKŞEHİR BELEDİYESİ ULAŞIM DARE BAŞKANLIĞI",
|
|
||||||
sehir: "İzmir",
|
|
||||||
bolge: "EGE BÖLGESİ",
|
|
||||||
tarih: "14.08.2026",
|
|
||||||
saat: "11:00",
|
|
||||||
alimTuru: "Açık İhale",
|
|
||||||
usul: "Açık İhale Usulü (4734 Madde 19)",
|
|
||||||
sektor: "İnşaat & Bakım",
|
|
||||||
brans: "HVAC ve İklimlendirme Bakımı",
|
|
||||||
kurumTipi: "Büyükşehir Belediyeleri",
|
|
||||||
eklenmeZamani: "3 saat önce",
|
|
||||||
isYeni: false,
|
|
||||||
aciliyetBadge: null,
|
|
||||||
items: [
|
|
||||||
{ id: "i1", ad: "G4 Panel Filtre 592x592x48mm", miktar: "120", birim: "adet", birimFiyat: 190 },
|
|
||||||
{ id: "i2", ad: "F7 Torba Filtre 592x592x535mm 6 Cepli", miktar: "80", birim: "adet", birimFiyat: 480 },
|
|
||||||
{ id: "i3", ad: "Hepa Filtre H13 610x610x292mm", miktar: "24", birim: "adet", birimFiyat: 2100 },
|
|
||||||
{ id: "i4", ad: "Periyodik Bakım ve Gaz Şarjı Hizmeti", miktar: "12", birim: "ay", birimFiyat: 15000 }
|
|
||||||
],
|
|
||||||
aciklama: "İzmir Büyükşehir Belediyesi Hizmet Binalarındaki 18 Adet Klima Santralinin 1 Yıllık Periyodik Filtre Değişimi ve Bakım Hizmeti Alımı İhalesidir.",
|
|
||||||
yaklasikMaliyet: 241000,
|
|
||||||
teslimatSuresi: "365 Gün",
|
|
||||||
teslimYeri: "İzmir Merkez Binaları",
|
|
||||||
yeterlikKriterleri: ["İş Deneyim Belgesi (Benzer İş %50)", "Geçici Teminat Mektubu %3", "Kapasite Raporu"],
|
|
||||||
aiMatchScore: 81,
|
|
||||||
aiRiskScore: "Orta",
|
|
||||||
aiBidRange: { min: 195000, max: 228000, recommended: 212000 },
|
|
||||||
kanbanStatus: "incelenecek",
|
|
||||||
atananPersonel: null,
|
|
||||||
durum: "Aktif",
|
|
||||||
incelendi: false,
|
|
||||||
takipte: false,
|
|
||||||
sozlesmeBedeli: null,
|
|
||||||
kazananFirma: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "tender-106",
|
|
||||||
ikn: "26DT1490888",
|
|
||||||
baslik: "Genel Temizlik ve Dezenfeksiyon Sarf Malzemeleri Alımı",
|
|
||||||
kurum: "ANTALYA GENÇLİK VE SPOR İL MÜDÜRLÜĞÜ KYK YURTLARI",
|
|
||||||
sehir: "Antalya",
|
|
||||||
bolge: "AKDENİZ BÖLGESİ",
|
|
||||||
tarih: "15.08.2026",
|
|
||||||
saat: "15:30",
|
|
||||||
alimTuru: "Doğrudan Temin",
|
|
||||||
usul: "Doğrudan Temin (4734 22/d)",
|
|
||||||
sektor: "Temizlik & Hijyen",
|
|
||||||
brans: "Endüstriyel Temizlik Kimyasalları",
|
|
||||||
kurumTipi: "Bakanlıklar",
|
|
||||||
eklenmeZamani: "5 saat önce",
|
|
||||||
isYeni: false,
|
|
||||||
aciliyetBadge: null,
|
|
||||||
items: [
|
|
||||||
{ id: "i1", ad: "Çamaşır Suyu (Yüzey Dezenfektanı) 20 L", miktar: "80", birim: "bidon", birimFiyat: 340 },
|
|
||||||
{ id: "i2", ad: "Sıvı El Sabunu Sedefli 30 L", miktar: "60", birim: "bidon", birimFiyat: 520 },
|
|
||||||
{ id: "i3", ad: "Mikrofiber Temizlik Bezi 40x40 (100'lü)", miktar: "15", birim: "paket", birimFiyat: 680 }
|
|
||||||
],
|
|
||||||
aciklama: "Antalya KYK Kız ve Erkek Öğrenci Yurtları yeni eğitim-öğretim sezonu hazırlıkları için temizlik hijyen sarf ürünleri temin edilecektir.",
|
|
||||||
yaklasikMaliyet: 68600,
|
|
||||||
teslimatSuresi: "7 Gün",
|
|
||||||
teslimYeri: "Antalya Akdeniz Yurt Müdürlüğü Deposu",
|
|
||||||
yeterlikKriterleri: ["MSDS Güvenlik Bilgi Formları", "Sağlık Bakanlığı Ruhsat Fotokopisi"],
|
|
||||||
aiMatchScore: 89,
|
|
||||||
aiRiskScore: "Düşük",
|
|
||||||
aiBidRange: { min: 54000, max: 63000, recommended: 58500 },
|
|
||||||
kanbanStatus: "kazanildi",
|
|
||||||
atananPersonel: "Hikmet Tanrıverdi",
|
|
||||||
durum: "Sonuçlandı",
|
|
||||||
incelendi: true,
|
|
||||||
takipte: true,
|
|
||||||
sozlesmeBedeli: 57400,
|
|
||||||
kazananFirma: "Tanrıverdi Medikal & Hijyen Ltd. Şti."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "tender-107",
|
|
||||||
ikn: "2026/172340",
|
|
||||||
baslik: "Veri Merkezi Sunucu ve Depolama Ünitesi Alımı",
|
|
||||||
kurum: "HAVELSAN HAVA ELEKTRONİK SANAYİ A.Ş.",
|
|
||||||
sehir: "Ankara",
|
|
||||||
bolge: "İÇ ANADOLU BÖLGESİ",
|
|
||||||
tarih: "18.08.2026",
|
|
||||||
saat: "10:00",
|
|
||||||
alimTuru: "Açık İhale",
|
|
||||||
usul: "Açık İhale",
|
|
||||||
sektor: "Bilişim & Yazılım",
|
|
||||||
brans: "Sunucu ve Veri Depolama Donanımları",
|
|
||||||
kurumTipi: "Kamu Şirketleri",
|
|
||||||
eklenmeZamani: "1 gün önce",
|
|
||||||
isYeni: false,
|
|
||||||
aciliyetBadge: null,
|
|
||||||
items: [
|
|
||||||
{ id: "i1", ad: "Rack Sunucu 2U 2x Xeon Gold 6430 / 256GB RAM", miktar: "4", birim: "adet", birimFiyat: 145000 },
|
|
||||||
{ id: "i2", ad: "NVMe SAN Storage Unit 100TB Usable", miktar: "1", birim: "set", birimFiyat: 620000 }
|
|
||||||
],
|
|
||||||
aciklama: "Havelsan Teknoloji Kampüsü Veri Merkezi Genişletme Projesi kapsamında yüksek performanslı sunucu ve veri depolama üniteleri temin edilecektir.",
|
|
||||||
yaklasikMaliyet: 1200000,
|
|
||||||
teslimatSuresi: "30 Takvim Günü",
|
|
||||||
teslimYeri: "Havelsan ODTÜ Teknokent Binası",
|
|
||||||
yeterlikKriterleri: ["Üretici Yetki Belgesi (MAF)", "ISO 27001 Bilgi Güvenliği Belgesi"],
|
|
||||||
aiMatchScore: 76,
|
|
||||||
aiRiskScore: "Yüksek",
|
|
||||||
aiBidRange: { min: 980000, max: 1150000, recommended: 1040000 },
|
|
||||||
kanbanStatus: "kaybedildi",
|
|
||||||
atananPersonel: "Mehmet Demir",
|
|
||||||
durum: "Sonuçlandı",
|
|
||||||
incelendi: true,
|
|
||||||
takipte: true,
|
|
||||||
sozlesmeBedeli: 1025000,
|
|
||||||
kazananFirma: "Netaş Telekomünikasyon A.Ş."
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export const MOCK_COMPANY_STATS = {
|
|
||||||
notlarimCount: 4,
|
|
||||||
ajandaCount: 2,
|
|
||||||
incelediklerimCount: 14,
|
|
||||||
sozlesmelerimCount: 8,
|
|
||||||
sonuclarimCount: 12,
|
|
||||||
takibimdekilerCount: 9,
|
|
||||||
kanbanStats: {
|
|
||||||
incelenecek: 3,
|
|
||||||
inceleniyor: 2,
|
|
||||||
teklif_hazirlaniyor: 4,
|
|
||||||
teklif_verildi: 3,
|
|
||||||
kazanildi: 8,
|
|
||||||
kaybedildi: 4
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MOCK_LIVE_STATS = {
|
|
||||||
onlineKullanici: 17551,
|
|
||||||
toplamKalem: 99666,
|
|
||||||
bugunEklenen: 1445,
|
|
||||||
aktifTakip: 102473
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SECTORS_LIST = [
|
|
||||||
"Sağlık / Tıbbi Malzeme",
|
|
||||||
"Kırtasiye & Büro",
|
|
||||||
"Bilişim & Yazılım",
|
|
||||||
"Gıda & Catering",
|
|
||||||
"İnşaat & Bakım",
|
|
||||||
"Temizlik & Hijyen",
|
|
||||||
"Tekstil & Giyim",
|
|
||||||
"Otomotiv & Yedek Parça"
|
|
||||||
];
|
|
||||||
|
|
||||||
export const INSTITUTION_TYPES_LIST = [
|
|
||||||
"Eğitim ve Araştırma Hastaneleri",
|
|
||||||
"Belediyeler",
|
|
||||||
"Büyükşehir Belediyeleri",
|
|
||||||
"Ceza İnfaz Kurumları",
|
|
||||||
"Üniversiteler",
|
|
||||||
"Bakanlıklar",
|
|
||||||
"Kamu Şirketleri",
|
|
||||||
"Ağız ve Diş Sağlığı Merkezleri"
|
|
||||||
];
|
|
||||||
|
|
||||||
export const CITIES_LIST = [
|
|
||||||
"Adana", "Adıyaman", "Afyonkarahisar", "Aksaray", "Amasya", "Ankara", "Antalya", "Ardahan", "Artvin",
|
|
||||||
"Aydın", "Balıkesir", "Bolu", "Bursa", "Çanakkale", "Denizli", "Diyarbakır", "Edirne", "Eskişehir",
|
|
||||||
"Gaziantep", "Giresun", "Isparta", "İstanbul", "İzmir", "Kahramanmaraş", "Kars", "Kastamonu", "Kayseri",
|
|
||||||
"Kocaeli", "Konya", "Kütahya", "Malatya", "Manisa", "Mardin", "Muğla", "Ordu", "Rize", "Sakarya",
|
|
||||||
"Samsun", "Sivas", "Şanlıurfa", "Tekirdağ", "Trabzon", "Van", "Zonguldak"
|
|
||||||
];
|
|
||||||
|
|
||||||
export const COMPETITOR_DATA = [
|
|
||||||
{ name: "Tanrıverdi Medikal & Hijyen Ltd.", winCount: 24, totalVolume: "4.850.000 TL", share: 32 },
|
|
||||||
{ name: "Atlas Büro & Kırtasiye A.Ş.", winCount: 19, totalVolume: "3.200.000 TL", share: 24 },
|
|
||||||
{ name: "ProTek Bilişim ve Sistemler", winCount: 15, totalVolume: "6.750.000 TL", share: 22 },
|
|
||||||
{ name: "Özkan Sağlık Hizmetleri", winCount: 11, totalVolume: "2.100.000 TL", share: 14 },
|
|
||||||
{ name: "Diğer Tedarikçiler", winCount: 8, totalVolume: "1.400.000 TL", share: 8 }
|
|
||||||
];
|
|
||||||
@@ -107,6 +107,63 @@ export function sectorStats(tenders) {
|
|||||||
return [...map.entries()].map(([sektor, count]) => ({ sektor, count })).sort((a, b) => b.count - a.count);
|
return [...map.entries()].map(([sektor, count]) => ({ sektor, count })).sort((a, b) => b.count - a.count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "10.08.2026" → Date
|
||||||
|
export function parseDateTR(s) {
|
||||||
|
if (!s) return null;
|
||||||
|
const m = String(s).match(/(\d{1,2})[.\/](\d{1,2})[.\/](\d{4})/);
|
||||||
|
if (!m) return null;
|
||||||
|
return new Date(Number(m[3]), Number(m[2]) - 1, Number(m[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Firmanın kendi performansı (kullanıcının kanban/takip durumundan) ---
|
||||||
|
const BID_STATES = ['teklif_verildi', 'kazanildi', 'kaybedildi'];
|
||||||
|
export function companyStats(tenders) {
|
||||||
|
const won = tenders.filter((t) => t.kanbanStatus === 'kazanildi');
|
||||||
|
const lost = tenders.filter((t) => t.kanbanStatus === 'kaybedildi');
|
||||||
|
const bid = tenders.filter((t) => BID_STATES.includes(t.kanbanStatus));
|
||||||
|
const wonTotal = won.reduce(
|
||||||
|
(s, t) => s + (parseTL(t.sozlesmeBedeli) ?? parseTL(t.yaklasikMaliyet) ?? 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const resulted = won.length + lost.length;
|
||||||
|
const kanban = {};
|
||||||
|
for (const st of ['incelenecek', 'inceleniyor', 'teklif_hazirlaniyor', 'teklif_verildi', 'kazanildi', 'kaybedildi']) {
|
||||||
|
kanban[st] = tenders.filter((t) => (t.kanbanStatus || '') === st).length;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
participated: bid.length,
|
||||||
|
won: won.length,
|
||||||
|
lost: lost.length,
|
||||||
|
resulted,
|
||||||
|
winRate: resulted ? Math.round((won.length / resulted) * 100) : 0,
|
||||||
|
wonTotal,
|
||||||
|
tracked: tenders.filter((t) => t.takipte).length,
|
||||||
|
reviewed: tenders.filter((t) => t.incelendi || (t.kanbanStatus && t.kanbanStatus !== 'incelenecek')).length,
|
||||||
|
contracts: won.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Aylık kazanım / katılım hacmi (son 7 ay) ---
|
||||||
|
const AY_KISA = ['Oca', 'Şub', 'Mar', 'Nis', 'May', 'Haz', 'Tem', 'Ağu', 'Eyl', 'Eki', 'Kas', 'Ara'];
|
||||||
|
export function monthlyStats(tenders) {
|
||||||
|
const map = new Map();
|
||||||
|
for (const t of tenders) {
|
||||||
|
if (!BID_STATES.includes(t.kanbanStatus)) continue;
|
||||||
|
const d = parseDateTR(t.tarih);
|
||||||
|
if (!d) continue;
|
||||||
|
const key = `${d.getFullYear()}-${String(d.getMonth()).padStart(2, '0')}`;
|
||||||
|
const cur = map.get(key) || { y: d.getFullYear(), m: d.getMonth(), kazanim: 0, katilim: 0 };
|
||||||
|
const val = parseTL(t.sozlesmeBedeli) ?? parseTL(t.yaklasikMaliyet) ?? 0;
|
||||||
|
cur.katilim += val;
|
||||||
|
if (t.kanbanStatus === 'kazanildi') cur.kazanim += val;
|
||||||
|
map.set(key, cur);
|
||||||
|
}
|
||||||
|
return [...map.values()]
|
||||||
|
.sort((a, b) => a.y - b.y || a.m - b.m)
|
||||||
|
.slice(-7)
|
||||||
|
.map((x) => ({ month: AY_KISA[x.m], kazanim: x.kazanim, katilim: x.katilim }));
|
||||||
|
}
|
||||||
|
|
||||||
// --- Alım türü dağılımı ---
|
// --- Alım türü dağılımı ---
|
||||||
export function alimTuruStats(tenders) {
|
export function alimTuruStats(tenders) {
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
|
|||||||
Reference in New Issue
Block a user