Require explicit persisted acknowledgement of notifications
This commit is contained in:
@@ -12,6 +12,7 @@ export const Header = () => {
|
||||
setIsAiSpecAnalyzerOpen,
|
||||
userProfile,
|
||||
notifications,
|
||||
refreshNotifications,markNotificationsRead,notificationError,notificationBusy,
|
||||
setActiveTab,
|
||||
clearAllFilters,
|
||||
isRefreshingLive,
|
||||
@@ -144,7 +145,7 @@ export const Header = () => {
|
||||
<div className="relative">
|
||||
<button
|
||||
aria-label="Bildirimler"
|
||||
onClick={() => setShowNotifications(!showNotifications)}
|
||||
onClick={() => {setShowNotifications(!showNotifications);if(!showNotifications)void refreshNotifications();}}
|
||||
className="relative p-2 text-slate-300 hover:text-white hover:bg-slate-700 rounded-lg transition-colors"
|
||||
>
|
||||
<Bell className="w-5 h-5" />
|
||||
@@ -157,13 +158,15 @@ export const Header = () => {
|
||||
<div className="absolute right-0 mt-2 w-80 bg-white text-slate-800 rounded-xl shadow-xl border border-slate-200 py-2 z-50 animate-fade-in">
|
||||
<div className="px-4 py-2 border-b border-slate-100 flex items-center justify-between">
|
||||
<h4 className="font-bold text-sm">Bildirimler</h4>
|
||||
<span className="text-xs text-blue-600 cursor-pointer hover:underline">Tümünü Okundu İşaretle</span>
|
||||
<button disabled={notificationBusy || !notifications.some(n=>!n.read)} onClick={markNotificationsRead} className="text-xs text-blue-600 hover:underline disabled:opacity-50">Tümünü Okundu İşaretle</button>
|
||||
</div>
|
||||
{notificationError&&<p role="alert" className="px-3 py-2 text-xs text-red-700">{notificationError} <button className="underline" onClick={refreshNotifications}>Bildirimleri yeniden yükle</button></p>}
|
||||
{notificationBusy&&<p role="status" className="px-3 text-xs">Okundu bilgisi kaydediliyor…</p>}
|
||||
<div className="max-h-64 overflow-y-auto divide-y divide-slate-100">
|
||||
{notifications.map(n => (
|
||||
<div key={n.id} className="p-3 hover:bg-slate-50 transition-colors cursor-pointer text-xs">
|
||||
<div className="font-semibold text-slate-900 flex justify-between">
|
||||
<span>{n.title}</span>
|
||||
<span>{!n.read&&<span className="text-blue-600 mr-1">Okunmadı ·</span>}{n.title}</span>
|
||||
<span className="text-slate-400 font-normal text-[10px]">{n.time}</span>
|
||||
</div>
|
||||
<p className="text-slate-600 mt-1 leading-relaxed">{n.text}</p>
|
||||
|
||||
+38
-17
@@ -1,3 +1,4 @@
|
||||
import {mergeServerNotifications,acknowledgeNotifications} from '../services/notifications';
|
||||
import { searchTender } from '../services/searchTender';
|
||||
import { normalizeTenderSearch, matchingItemResults } from '../services/tenderSearch';
|
||||
import React, {createContext, useContext, useState, useEffect, useMemo, useRef } from 'react';
|
||||
@@ -159,6 +160,7 @@ export const AppProvider = ({ children }) => {
|
||||
|
||||
// Giriş yapınca kullanıcı verisini SUNUCUDAN yükle (localStorage'ı ezer)
|
||||
useEffect(() => {
|
||||
notificationVersion.current++;notificationMutation.current=null;seenServerNotifications.current.clear();setNotificationError(null);setNotificationBusy(false);
|
||||
aiRunRef.current=null;setAiAnalyzing(false);setAiJobStatus('');
|
||||
setVeriSenkron(false); setPlan(null); setSaveError(null);
|
||||
setFavoriteKurumlar([]); setTeamMembers([]); setSavedFilters([]); setCompetitorWatchlist([]);
|
||||
@@ -564,11 +566,17 @@ export const AppProvider = ({ children }) => {
|
||||
};
|
||||
|
||||
const [notifications, setNotifications] = useState([]);
|
||||
const [notificationError,setNotificationError]=useState(null);
|
||||
const [notificationBusy,setNotificationBusy]=useState(false);
|
||||
const notificationVersion=useRef(0),notificationMutation=useRef(null),seenServerNotifications=useRef(new Set());
|
||||
|
||||
// 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));
|
||||
pushNotification(title,text);
|
||||
};
|
||||
const pushNotification=(title,text)=>{
|
||||
if (isQuietNow() || !reminderSettings.pushEnabled) return;
|
||||
try {
|
||||
if ('Notification' in window && Notification.permission === 'granted') {
|
||||
@@ -581,7 +589,31 @@ export const AppProvider = ({ children }) => {
|
||||
if (Notification.permission === 'granted') return 'granted';
|
||||
try { return await Notification.requestPermission(); } catch { return 'denied'; }
|
||||
};
|
||||
const markNotificationsRead = () => setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
|
||||
const refreshNotifications=async()=>{
|
||||
const session=getToken(),version=++notificationVersion.current;
|
||||
setNotificationError(null);
|
||||
try {
|
||||
const rows=await bildirimleriGetir();
|
||||
if(getToken()!==session||version!==notificationVersion.current)return;
|
||||
const checked=mergeServerNotifications([],rows);
|
||||
const fresh=checked.filter(row=>!row.read&&!seenServerNotifications.current.has(row.id));
|
||||
checked.forEach(row=>seenServerNotifications.current.add(row.id));
|
||||
fresh.slice(0,5).forEach(row=>pushNotification(row.title,row.text));
|
||||
setNotifications(current=>[...checked,...current.filter(row=>!row.serverId)].slice(0,80));
|
||||
}catch(error){if(getToken()===session&&version===notificationVersion.current)setNotificationError(error.message);}
|
||||
};
|
||||
const markNotificationsRead=async()=>{
|
||||
if(notificationMutation.current)return;
|
||||
const operation={session:getToken()};notificationMutation.current=operation;
|
||||
notificationVersion.current++;setNotificationBusy(true);setNotificationError(null);
|
||||
try {
|
||||
const ids=await acknowledgeNotifications(notifications,bildirimOkundu);
|
||||
if(getToken()!==operation.session||notificationMutation.current!==operation)return;
|
||||
notificationVersion.current++;
|
||||
setNotifications(current=>current.map(row=>ids.has(row.id)?{...row,read:true}:row));
|
||||
}catch(error){if(getToken()===operation.session&¬ificationMutation.current===operation)setNotificationError(error.message);}
|
||||
finally {if(notificationMutation.current===operation){notificationMutation.current=null;setNotificationBusy(false);}}
|
||||
};
|
||||
|
||||
// Persist state to localstorage
|
||||
// Havuz on binlerce kayda çıkabiliyor; tamamı localStorage kotasını (≈5 MB)
|
||||
@@ -645,22 +677,10 @@ export const AppProvider = ({ children }) => {
|
||||
// Sunucu bildirimleri (zeyilname / sonuç): 5 dk'da bir çek, yenileri göster
|
||||
useEffect(() => {
|
||||
if (!veriSenkron) return;
|
||||
let iptal = false;
|
||||
const session = getToken();
|
||||
const cek = async () => {
|
||||
try {
|
||||
const liste = await bildirimleriGetir();
|
||||
const yeni = liste.filter((b) => !b.okundu);
|
||||
if (iptal || getToken() !== session || !yeni.length) return;
|
||||
for (const b of yeni.slice(0, 5)) notify(b.baslik, b.metin);
|
||||
await bildirimOkundu(yeni.map((b) => b.id));
|
||||
} catch { /* sessiz */ }
|
||||
};
|
||||
cek();
|
||||
const z = setInterval(cek, 5 * 60 * 1000);
|
||||
return () => { iptal = true; clearInterval(z); };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [veriSenkron]);
|
||||
void refreshNotifications();
|
||||
const timer=setInterval(refreshNotifications,5*60*1000);
|
||||
return()=>{notificationVersion.current++;clearInterval(timer);};
|
||||
}, [veriSenkron,authUser?.email]);
|
||||
|
||||
// Şirket belgesi süre uyarısı: süresi dolmuş / 30 gün içinde dolacak
|
||||
// belgeler için günde bir kez bildirim (kimse teklif hazırlanana kadar
|
||||
@@ -807,6 +827,7 @@ export const AppProvider = ({ children }) => {
|
||||
notify,
|
||||
requestPushPermission,
|
||||
markNotificationsRead,
|
||||
refreshNotifications,notificationError,notificationBusy,
|
||||
isQuietNow,
|
||||
favoriteKurumlar,
|
||||
toggleFavoriteKurum,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export function mergeServerNotifications(current,rows) {
|
||||
if(!Array.isArray(rows)||rows.some(row=>typeof row?.id!=='string'||typeof row.baslik!=='string'||typeof row.metin!=='string'))throw new Error('Bildirim listesi geçersiz.');
|
||||
const remote=rows.map(row=>({id:'server:'+row.id,serverId:row.id,title:row.baslik,text:row.metin,
|
||||
time:new Date(row.zaman).toLocaleString('tr-TR'),read:!!row.okundu,ikn:row.ikn}));
|
||||
return [...remote,...current.filter(row=>!row.serverId)].slice(0,80);
|
||||
}
|
||||
export async function acknowledgeNotifications(snapshot,request) {
|
||||
const unread=snapshot.filter(row=>!row.read),ids=unread.filter(row=>row.serverId).map(row=>row.serverId);
|
||||
if(ids.length){const result=await request(ids);if(result?.success!==true||result.okundu!==ids.length)throw new Error('Bazı bildirimler kaydedilemedi. Listeyi yenileyip tekrar deneyin.');}
|
||||
return new Set(unread.map(row=>row.id));
|
||||
}
|
||||
Reference in New Issue
Block a user