feat: demo özelliklerini gerçekleştir — canlı veri + localStorage

- analytics.js: firmStats/itemStats/provinceDensity (gerçek hesaplama)
- Rakip Radar: canlı veriden kazanan firma analizi (mock 3 firma kaldırıldı)
- Fiyat Analizi: gerçek kalem min/medyan/ort/maks + CSV export
- TurkeyHeatMap: 81 il gerçek interaktif yoğunluk haritası (tıkla→filtrele)
- Favori kurumlar: kart yıldızı + hızlı filtre (localStorage)
- Ekip yönetimi: gerçek CRUD (RightSidebar) + Kanban ataması ekipten
- Bildirimler: gerçek Notification API + izin + test + sessiz saatler
- PDF: jsPDF ile gerçek birim fiyat teklif cetveli (kaşe/imza alanı)
This commit is contained in:
2026-08-28 01:58:46 +03:00
parent 512d529af2
commit 0ab74143c0
16 changed files with 895 additions and 154 deletions
+81 -9
View File
@@ -113,6 +113,35 @@ export const AppProvider = ({ children }) => {
{ 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 }
]);
// --- Favori kurumlar (localStorage, gerçek) ---
const [favoriteKurumlar, setFavoriteKurumlar] = useState(() => {
try { return JSON.parse(localStorage.getItem('ihaleist_favori_kurumlar') || '[]'); } catch { return []; }
});
useEffect(() => {
localStorage.setItem('ihaleist_favori_kurumlar', JSON.stringify(favoriteKurumlar));
}, [favoriteKurumlar]);
const toggleFavoriteKurum = (kurum) => {
const ad = (kurum || '').trim();
if (!ad) return;
setFavoriteKurumlar((prev) => (prev.includes(ad) ? prev.filter((k) => k !== ad) : [...prev, ad]));
};
// --- Ekip yönetimi (localStorage, gerçek CRUD) ---
const [teamMembers, setTeamMembers] = useState(() => {
try {
const saved = localStorage.getItem('ihaleist_ekip');
if (saved) return JSON.parse(saved);
} catch { /* yoksay */ }
return [
{ id: 'tm-1', name: 'Hikmet Tanrıverdi', email: 'hikmet044@gmail.com', role: 'Yönetici' },
];
});
useEffect(() => {
localStorage.setItem('ihaleist_ekip', JSON.stringify(teamMembers));
}, [teamMembers]);
const addTeamMember = (m) => setTeamMembers((prev) => [...prev, { id: 'tm-' + Date.now(), ...m }]);
const removeTeamMember = (id) => setTeamMembers((prev) => prev.filter((m) => m.id !== id));
const [vaultDocuments, setVaultDocuments] = useState([
{ 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' },
{ 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' },
@@ -121,16 +150,32 @@ export const AppProvider = ({ children }) => {
{ 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' }
]);
const [reminderSettings, setReminderSettings] = useState({
emailEnabled: true,
smsEnabled: true,
pushEnabled: true,
notify3DaysBefore: true,
notify1DayBefore: true,
notify3HoursBefore: true,
googleCalendarSync: true,
outlookSync: false
const [reminderSettings, setReminderSettings] = useState(() => {
const def = {
emailEnabled: true, smsEnabled: true, pushEnabled: true,
notify3DaysBefore: true, notify1DayBefore: true, notify3HoursBefore: true,
googleCalendarSync: true, outlookSync: false,
quietEnabled: false, quietStart: '22:00', quietEnd: '08:00',
};
try {
const saved = JSON.parse(localStorage.getItem('ihaleist_reminder') || 'null');
return saved ? { ...def, ...saved } : def;
} catch { return def; }
});
useEffect(() => {
localStorage.setItem('ihaleist_reminder', JSON.stringify(reminderSettings));
}, [reminderSettings]);
// Şu an sessiz saat aralığında mıyız (gece yarısını geçen aralıkları da destekler)
const isQuietNow = () => {
if (!reminderSettings.quietEnabled) return false;
const now = new Date();
const cur = now.getHours() * 60 + now.getMinutes();
const [sh, sm] = reminderSettings.quietStart.split(':').map(Number);
const [eh, em] = reminderSettings.quietEnd.split(':').map(Number);
const start = sh * 60 + sm, end = eh * 60 + em;
return start <= end ? cur >= start && cur < end : cur >= start || cur < end;
};
const [webhooksConfig, setWebhooksConfig] = useState({
slackWebhookUrl: 'https://hooks.slack.com/services/T00/B00/XXXXXX',
@@ -279,6 +324,24 @@ export const AppProvider = ({ children }) => {
{ 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ı
// bildirimi (Notification API) da gösterir.
const notify = (title, text) => {
setNotifications((prev) => [{ id: 'n-' + Date.now(), title, text, time: 'az önce', read: false }, ...prev].slice(0, 30));
if (isQuietNow() || !reminderSettings.pushEnabled) return;
try {
if ('Notification' in window && Notification.permission === 'granted') {
new Notification(title, { body: text, icon: '/favicon.ico' });
}
} catch { /* yoksay */ }
};
const requestPushPermission = async () => {
if (!('Notification' in window)) return 'unsupported';
if (Notification.permission === 'granted') return 'granted';
try { return await Notification.requestPermission(); } catch { return 'denied'; }
};
const markNotificationsRead = () => setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
// Persist state to localstorage
useEffect(() => {
localStorage.setItem('ihaleist_tenders', JSON.stringify(tenders));
@@ -410,6 +473,15 @@ export const AppProvider = ({ children }) => {
activeAiModel,
userProfile,
notifications,
notify,
requestPushPermission,
markNotificationsRead,
isQuietNow,
favoriteKurumlar,
toggleFavoriteKurum,
teamMembers,
addTeamMember,
removeTeamMember,
toggleTrackTender,
updateKanbanStatus,
clearAllFilters