fix: persist notification preferences and clarify delivery controls

This commit is contained in:
2026-09-06 14:14:46 +03:00
parent 83df706631
commit a25dcb7a5d
8 changed files with 85 additions and 35 deletions
+16 -18
View File
@@ -2,12 +2,11 @@ import {DeliveryQueue} from './DeliveryQueue';
import React, { useState, useEffect } from 'react';
import { useApp } from '../../context/AppContext';
import { Smartphone, Wifi, WifiOff, Download, Bell, CheckCircle2, Moon, Mail, Send } from 'lucide-react';
import confetti from 'canvas-confetti';
import { getToken } from '../../services/auth';
import { NotifySubscriptions } from './NotifySubscriptions';
export const PwaSettings = () => {
const { userProfile, authUser, tenders, reminderSettings, setReminderSettings, requestPushPermission, notify, isQuietNow } = useApp();
const { userProfile, authUser, tenders, reminderSettings, saveReminderSettings, reminderSaving, reminderError, requestPushPermission, notify, isQuietNow } = useApp();
// Gerçek Cache API sayımı — sahte "102 ihale önbellekte" yerine
const [onbellekSayisi, setOnbellekSayisi] = useState(null);
const [onbellekAdi, setOnbellekAdi] = useState('');
@@ -73,12 +72,10 @@ export const PwaSettings = () => {
const p = await requestPushPermission();
setPermission(p);
if (p === 'granted') {
setReminderSettings((s) => ({ ...s, pushEnabled: true }));
notify('Bildirimler açıldı', 'Yeni ihale ve son tarih uyarıları buradan gelecek.');
confetti({ particleCount: 50 });
await saveReminderSettings((s) => ({ ...s, pushEnabled: true }));
}
};
const setQuiet = (patch) => setReminderSettings((s) => ({ ...s, ...patch }));
const setQuiet = (patch) => saveReminderSettings((s) => ({ ...s, ...patch }));
// Gerçek PWA kurulumu: tarayıcı beforeinstallprompt verirse yerel diyalog;
// zaten standalone açıldıysa "yüklü"; iOS Safari'de olay yok → yol tarifi.
@@ -173,36 +170,42 @@ export const PwaSettings = () => {
</div>
<p className="text-xs text-slate-600 leading-relaxed">
Tarayıcı bildirimlerine izin verin; yeni ihale ve son tarih uyarıları gerçek bildirim olarak gelir.
Bu uygulama açıkken alınan uyarıları tarayıcı bildirimi olarak gösterebilirsiniz. Kapalı uygulamaya arka plan push teslimi sağlanmaz.
</p>
{permission !== 'granted' ? (
<button onClick={handleEnablePush} disabled={permission === 'unsupported'}
<button onClick={handleEnablePush} disabled={reminderSaving || permission === 'unsupported'}
className="w-full bg-amber-500 hover:bg-amber-400 disabled:opacity-40 text-white font-bold text-xs py-2.5 rounded-xl">
Bildirimlere İzin Ver
</button>
) : (
<button onClick={() => notify('Test bildirimi', isQuietNow() ? 'Sessiz saatte — sadece listeye eklendi.' : 'Bildirimler çalışıyor ✓')}
<button onClick={() => notify('Test bildirimi', isQuietNow() || !reminderSettings.pushEnabled ? 'Tarayıcı bildirimi kapalı veya sessiz saatte — listeye eklendi.' : 'Tarayıcı bildirimi gösterim denemesi.')}
className="w-full bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs py-2.5 rounded-xl">
Test Bildirimi Gönder
</button>
)}
{reminderError&&<p role="alert" className="text-xs text-red-700">{reminderError}</p>}
{reminderSaving&&<p role="status" className="text-xs">Bildirim ayarları kaydediliyor…</p>}
<label className="flex justify-between text-xs font-bold">Tarayıcı bildirimlerini göster
<input type="checkbox" checked={reminderSettings.pushEnabled} disabled={reminderSaving} onChange={e=>setQuiet({pushEnabled:e.target.checked})}/>
</label>
{/* Sessiz saatler — gerçek */}
<div className="pt-2 border-t border-slate-100 space-y-2">
<p className="text-xs text-slate-500">Saatler bu cihazın yerel saatine göredir. Başlangıç ve bitiş aynıysa sessiz süre uygulanmaz.</p>
<label className="flex items-center justify-between text-xs font-bold text-slate-800 cursor-pointer">
<span className="flex items-center gap-1.5"><Moon className="w-3.5 h-3.5 text-indigo-500" /> Sessiz Saatler</span>
<input type="checkbox" checked={reminderSettings.quietEnabled}
<input type="checkbox" disabled={reminderSaving} checked={reminderSettings.quietEnabled}
onChange={(e) => setQuiet({ quietEnabled: e.target.checked })}
className="w-4 h-4 accent-indigo-600 rounded" />
</label>
{reminderSettings.quietEnabled && (
<div className="flex items-center gap-2 text-xs">
<input type="time" value={reminderSettings.quietStart}
<input type="time" aria-label="Sessiz saat başlangıcı" disabled={reminderSaving} value={reminderSettings.quietStart}
onChange={(e) => setQuiet({ quietStart: e.target.value })}
className="bg-slate-50 border border-slate-300 rounded-lg px-2 py-1.5 flex-1" />
<span className="text-slate-400">→</span>
<input type="time" value={reminderSettings.quietEnd}
<input type="time" aria-label="Sessiz saat bitişi" disabled={reminderSaving} value={reminderSettings.quietEnd}
onChange={(e) => setQuiet({ quietEnd: e.target.value })}
className="bg-slate-50 border border-slate-300 rounded-lg px-2 py-1.5 flex-1" />
</div>
@@ -234,12 +237,7 @@ export const PwaSettings = () => {
: '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>
<p className="text-xs text-slate-600">E-posta gönderimlerini aşağıdaki bildirim aboneliklerinden yönetin. Tarayıcı bildirim seçimi ve sessiz saatler e-posta aboneliklerini kapatmaz.</p>
<button
onClick={sendTestEmail}
+19 -16
View File
@@ -1,3 +1,4 @@
import {validateReminderPreferences,quietNow} from '../services/reminderPreferences';
import {validateTeam} from '../services/teamData';
import {savedFilterCriteria} from '../services/savedFilterCriteria';
import {mergeServerNotifications,acknowledgeNotifications} from '../services/notifications';
@@ -166,7 +167,7 @@ export const AppProvider = ({ children }) => {
useEffect(() => {
notificationVersion.current++;notificationMutation.current=null;seenServerNotifications.current.clear();setNotificationError(null);setNotificationBusy(false);
aiRunRef.current=null;setAiAnalyzing(false);setAiJobStatus('');
dataRevisions.current={};setTeamError(null);setCompetitorError(null);
dataRevisions.current={};setTeamError(null);setCompetitorError(null);setReminderError(null);
setVeriSenkron(false); setPlan(null); setSaveError(null);
setFavoriteKurumlar([]); setTeamMembers([]); setSavedFilters([]); setCompetitorWatchlist([]);
setReminderSettings(defaultReminder()); setUserProfile(defaultProfile(authUser));
@@ -409,20 +410,22 @@ export const AppProvider = ({ children }) => {
const [vaultDocuments, setVaultDocuments] = useState([]);
const [reminderSettings, setReminderSettings] = useState(defaultReminder);
useEffect(() => {
if (veriSenkron) ilkYazmaDegil('reminder', reminderSettings);
}, [reminderSettings, veriSenkron]);
// Ş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 reminderSaveLock=useRef(false),reminderCurrent=useRef(reminderSettings);
reminderCurrent.current=reminderSettings;
const [reminderSaving,setReminderSaving]=useState(false),[reminderError,setReminderError]=useState(null);
const saveReminderSettings=async change=>{
if(!authUser||!veriSenkron){setReminderError('Hesap verileri yüklenmeden bildirim ayarları kaydedilemez.');return false;}
if(reminderSaveLock.current)return false;
const session=getToken();reminderSaveLock.current=true;setReminderSaving(true);setReminderError(null);
try {
const next=validateReminderPreferences(typeof change==='function'?change(reminderCurrent.current):change);
const saved=await setUserData('reminder',next,dataRevisions.current.reminder ?? null);
if(getToken()!==session)return false;
dataRevisions.current.reminder=saved.revision;reminderCurrent.current=next;setReminderSettings(next);return true;
}catch(error){if(getToken()===session)setReminderError(error.message);return false;}
finally{reminderSaveLock.current=false;setReminderSaving(false);}
};
const isQuietNow=()=>quietNow(reminderCurrent.current);
const [webhooksConfig, setWebhooksConfig] = useState({
slackWebhookUrl: '',
@@ -580,7 +583,7 @@ export const AppProvider = ({ children }) => {
pushNotification(title,text);
};
const pushNotification=(title,text)=>{
if (isQuietNow() || !reminderSettings.pushEnabled) return;
if (isQuietNow() || !reminderCurrent.current.pushEnabled) return;
try {
if ('Notification' in window && Notification.permission === 'granted') {
new Notification(title, { body: text, icon: '/favicon.ico' });
@@ -826,7 +829,7 @@ export const AppProvider = ({ children }) => {
vaultDocuments,
setVaultDocuments,
reminderSettings,
setReminderSettings,
saveReminderSettings,reminderSaving,reminderError,
webhooksConfig,
setWebhooksConfig,
apiKeys,
+13
View File
@@ -0,0 +1,13 @@
export function validateReminderPreferences(value) {
if(!value || typeof value!=='object' || Array.isArray(value))throw new Error('Bildirim ayarları geçersiz.');
for(const key of ['pushEnabled','quietEnabled'])if(typeof value[key]!=='boolean')throw new Error('Bildirim seçimini kontrol edin.');
for(const key of ['quietStart','quietEnd'])if(typeof value[key]!=='string'||!/^([01]\d|2[0-3]):[0-5]\d$/.test(value[key]))throw new Error('Sessiz saatleri saat:dakika biçiminde girin.');
return value;
}
export function quietNow(preferences,now=new Date()) {
if(!preferences?.quietEnabled)return false;
try {validateReminderPreferences(preferences);}catch{return true;}
const minute=text=>Number(text.slice(0,2))*60+Number(text.slice(3));
const start=minute(preferences.quietStart),end=minute(preferences.quietEnd),current=now.getHours()*60+now.getMinutes();
return start<=end ? current>=start&&current<end : current>=start||current<end;
}