Files
ihaleist/src/components/tools/PwaSettings.jsx
T

311 lines
16 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
// Gerçek Cache API sayımı — sahte "102 ihale önbellekte" yerine
const [onbellekSayisi, setOnbellekSayisi] = useState(null);
const [onbellekAdi, setOnbellekAdi] = useState('');
const [cacheLoading, setCacheLoading] = useState(false);
const [cacheError, setCacheError] = useState(null);
const [online, setOnline] = useState(navigator.onLine);
const refreshCacheStatus = async () => {
setCacheLoading(true); setCacheError(null);
try {
if (!('caches' in window)) throw new Error('Bu tarayıcı uygulama önbelleğini desteklemiyor.');
const adlar = (await caches.keys()).filter(name => name.startsWith('ihaleist-'));
let toplam = 0;
for (const ad of adlar) toplam += (await (await caches.open(ad)).keys()).length;
setOnbellekSayisi(toplam); setOnbellekAdi(adlar.join(', '));
} catch (error) { setCacheError(error.message); setOnbellekSayisi(null); }
finally { setCacheLoading(false); }
};
useEffect(() => { refreshCacheStatus(); }, []);
useEffect(() => {
const changed = () => setOnline(navigator.onLine);
window.addEventListener('online', changed); window.addEventListener('offline', changed);
return () => { window.removeEventListener('online', changed); window.removeEventListener('offline', changed); };
}, []);
const [installed, setInstalled] = useState(false);
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 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 });
}
};
const setQuiet = (patch) => setReminderSettings((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.
const [kurulumOlayi, setKurulumOlayi] = useState(null);
const [iosIpucu, setIosIpucu] = useState(false);
useEffect(() => {
const standalone = window.matchMedia?.('(display-mode: standalone)')?.matches || window.navigator.standalone === true;
if (standalone) setInstalled(true);
const yakala = (e) => { e.preventDefault(); setKurulumOlayi(e); };
const kuruldu = () => { setInstalled(true); setKurulumOlayi(null); };
window.addEventListener('beforeinstallprompt', yakala);
window.addEventListener('appinstalled', kuruldu);
return () => { window.removeEventListener('beforeinstallprompt', yakala); window.removeEventListener('appinstalled', kuruldu); };
}, []);
const handleInstallPwa = async () => {
if (kurulumOlayi) {
kurulumOlayi.prompt();
const { outcome } = await kurulumOlayi.userChoice.catch(() => ({ outcome: 'dismissed' }));
if (outcome === 'accepted') setInstalled(true);
setKurulumOlayi(null);
return;
}
setIosIpucu(true); // olay yoksa (iOS / zaten yüklü / desteklenmiyor) yol tarifi göster
};
return (
<div className="space-y-6 animate-fade-in">
{/* Header Banner */}
<div className="bg-linear-to-r from-slate-900 via-blue-950 to-cyan-900 text-white p-6 rounded-2xl shadow-lg border border-cyan-800/40 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-cyan-600 text-white flex items-center justify-center font-black shadow-md">
<Smartphone className="w-6 h-6" />
</div>
<div>
<h2 className="font-extrabold text-xl">Mobil uygulama ve bağlantı durumu</h2>
<p className="text-xs text-cyan-200 mt-0.5">Uygulama kabuğu önbellekten açılabilir. İhale güncelleme, belge erişimi ve hazırlık kaydı için internet bağlantısı gerekir.</p>
</div>
</div>
</div>
{/* Main Controls Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Offline Cache Status */}
<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">
{online ? <Wifi className="w-4 h-4 text-emerald-600" /> : <WifiOff className="w-4 h-4 text-slate-400" />} Çevrimdışı (Offline) Önbellek
</h3>
<span className="bg-emerald-100 text-emerald-800 font-extrabold text-[10px] px-2 py-0.5 rounded-full">
{online ? 'Bağlantı var' : 'Çevrimdışı'}
</span>
</div>
<div className="space-y-2 text-xs text-slate-700">
<p className="flex justify-between">
<span>İhale Kayıtları (bellekte):</span>
<strong className="text-slate-900">{tenders.length.toLocaleString('tr-TR')} ihale</strong>
</p>
<p className="flex justify-between">
<span>Önbelleğe alınmış istek:</span>
<strong className="text-slate-900">{onbellekSayisi == null ? '—' : `${onbellekSayisi} kayıt`}</strong>
</p>
<p className="flex justify-between">
<span>Uygulama sürümü önbelleği:</span>
<strong className="text-emerald-700">{onbellekAdi || '—'}</strong>
</p>
</div>
<button
onClick={refreshCacheStatus} disabled={cacheLoading}
className="w-full bg-slate-100 hover:bg-slate-200 text-slate-800 font-bold text-xs py-2.5 rounded-xl border border-slate-200 transition-colors"
>
{cacheLoading ? 'Kontrol ediliyor…' : 'Önbellek durumunu kontrol et'}
</button>
{cacheError && <p role="alert" className="text-xs text-red-700">{cacheError}</p>}
<p className="text-xs text-slate-600">Bağlantı durumu sunucuya erişimi garanti etmez. Çevrimdışı değişiklikler otomatik gönderim kuyruğuna alınmaz; kayıt sonucunu kontrol edin.</p>
</div>
{/* Gerçek Tarayıcı Bildirimleri + Sessiz Saatler */}
<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">
<Bell className="w-4 h-4 text-amber-500" /> Anlık Bildirimler
</h3>
<span className={`font-extrabold text-[10px] px-2 py-0.5 rounded-full ${
permission === 'granted' ? 'bg-emerald-100 text-emerald-800' : permission === 'denied' ? 'bg-red-100 text-red-700' : 'bg-slate-100 text-slate-600'
}`}>
{permission === 'granted' ? 'İzin Verildi' : permission === 'denied' ? 'Reddedildi' : permission === 'unsupported' ? 'Desteklenmiyor' : 'İzin Bekliyor'}
</span>
</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.
</p>
{permission !== 'granted' ? (
<button onClick={handleEnablePush} disabled={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 ✓')}
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>
)}
{/* Sessiz saatler — gerçek */}
<div className="pt-2 border-t border-slate-100 space-y-2">
<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}
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}
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}
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>
)}
{reminderSettings.quietEnabled && (
<p className="text-[10px] text-slate-400">
{isQuietNow() ? '🔕 Şu an sessiz saatte — tarayıcı bildirimi gösterilmez, uyarılar listeye eklenir.' : '🔔 Şu an aktif — bildirimler gösterilir.'}
</p>
)}
</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 */}
<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">
<h3 className="font-black text-sm text-white flex items-center gap-2">
<Download className="w-4 h-4 text-cyan-400" /> İhaleist Mobil / Masaüstü
</h3>
<span className="bg-cyan-500 text-slate-950 font-black text-[10px] px-2 py-0.5 rounded-full">
PWA 2.0
</span>
</div>
<p className="text-xs text-slate-300 leading-relaxed">
İhaleist'i iOS, Android, Windows veya Mac cihazınıza bir tıklama ile uygulama olarak kurun.
</p>
<button
onClick={handleInstallPwa}
disabled={installed}
className="w-full bg-cyan-500 hover:bg-cyan-400 disabled:bg-slate-700 text-slate-950 font-black text-xs py-3 rounded-xl flex items-center justify-center gap-2 shadow-md transition-colors cursor-pointer"
>
{installed ? (
<>
<CheckCircle2 className="w-4 h-4 text-emerald-950" />
<span>Uygulama olarak yüklü</span>
</>
) : (
<>
<Download className="w-4 h-4 text-slate-950" />
<span>İhaleist Uygulamasını Cihaza Yükle</span>
</>
)}
</button>
{iosIpucu && !installed && (
<div className="bg-white/10 border border-white/15 rounded-xl p-3 text-[11px] text-slate-200 leading-relaxed space-y-1">
<p><strong className="text-white">iPhone / iPad (Safari):</strong> alttaki Paylaş simgesi → <strong>Ana Ekrana Ekle</strong>.</p>
<p><strong className="text-white">Android (Chrome):</strong> sağ üst ⋮ menü → <strong>Uygulamayı yükle</strong> / Ana ekrana ekle.</p>
<p><strong className="text-white">Masaüstü (Chrome/Edge):</strong> adres çubuğunun sağındaki yükleme simgesi.</p>
</div>
)}
</div>
</div>
{/* Otomatik bildirim abonelikleri */}
<NotifySubscriptions />
<DeliveryQueue />
</div>
);
};