// Profile page — redesigned to match app style const PRFIcons = window.Icons; function pwStrength(pw) { if (!pw) return 0; let s = 0; if (pw.length >= 8) s++; if (pw.length >= 12) s++; if (/[A-Z]/.test(pw)) s++; if (/[0-9]/.test(pw)) s++; if (/[^A-Za-z0-9]/.test(pw)) s++; return s; } const PW_LABELS = ['', 'Çok zayıf', 'Zayıf', 'Orta', 'İyi', 'Güçlü']; const PW_COLORS = ['', '#ef4444', '#f97316', '#f59e0b', '#22c55e', '#16a34a']; function calcStreak(tasks) { const today = new Date(); today.setHours(0,0,0,0); const doneDays = new Set( tasks.filter(t => t.status === 'done').map(t => { const d = t.completedAt ? new Date(t.completedAt) : t.updatedAt ? new Date(t.updatedAt) : null; if (!d) return null; const dd = new Date(d); dd.setHours(0,0,0,0); return dd.getTime(); }).filter(Boolean) ); let streak = 0; for (let i = 0; i < 365; i++) { const d = new Date(today); d.setDate(d.getDate() - i); if (!doneDays.has(d.getTime())) break; streak++; } return streak; } function Profile({ user, onUpdateUser, setActiveView, onLogout, accentColor, onSetAccentColor, state, theme, onSetTheme, onReplaceState }) { const [name, setName] = React.useState(user?.name || ''); const [email, setEmail] = React.useState(user?.email || ''); const [title, setTitle] = React.useState(user?.title || ''); const [phone, setPhone] = React.useState(user?.phone || ''); const [bio, setBio] = React.useState(user?.bio || ''); const [profileMsg, setProfileMsg] = React.useState(null); const [profileBusy, setProfileBusy] = React.useState(false); const [curPw, setCurPw] = React.useState(''); const [newPw, setNewPw] = React.useState(''); const [confPw, setConfPw] = React.useState(''); const [pwMsg, setPwMsg] = React.useState(null); const [pwBusy, setPwBusy] = React.useState(false); const strength = pwStrength(newPw); const [avatarUrl, setAvatarUrl] = React.useState(user?.avatarUrl || null); const [avatarBusy, setAvatarBusy] = React.useState(false); const fileRef = React.useRef(null); const importRef = React.useRef(null); const [defaultView, setDefaultView] = React.useState(user?.defaultProjectView || 'kanban'); const [settingsSaved, setSettingsSaved] = React.useState(false); const [exportMsg, setExportMsg] = React.useState(null); // Digest bildirim ayarları const [digestEnabled, setDigestEnabled] = React.useState(user?.digestEnabled ?? false); const [digestHour, setDigestHour] = React.useState(user?.digestHour ?? 8); const [digestIncludeOverdue,setDigestIncludeOverdue]= React.useState(user?.digestIncludeOverdue ?? true); const [digestProjects, setDigestProjects] = React.useState(user?.digestProjects || []); const [digestMsg, setDigestMsg] = React.useState(null); const [digestBusy, setDigestBusy] = React.useState(false); const [pushSupported, setPushSupported] = React.useState(false); const [pushSubscribed, setPushSubscribed] = React.useState(false); const [pushBusy, setPushBusy] = React.useState(false); const [pushMsg, setPushMsg] = React.useState(null); React.useEffect(() => { if (!('serviceWorker' in navigator) || !('PushManager' in window)) return; setPushSupported(true); navigator.serviceWorker.ready.then(reg => reg.pushManager.getSubscription().then(sub => setPushSubscribed(!!sub)) ).catch(() => {}); }, []); const urlBase64ToUint8Array = (base64String) => { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); const raw = atob(base64); return Uint8Array.from([...raw].map(c => c.charCodeAt(0))); }; const enablePush = async () => { setPushBusy(true); setPushMsg(null); try { const perm = await Notification.requestPermission(); if (perm !== 'granted') { setPushMsg({ ok: false, text: 'Bildirim izni verilmedi.' }); return; } const keyRes = await fetch('./api/push.php', { credentials: 'include' }); const keyData = await keyRes.json(); if (!keyData.ok) throw new Error('Anahtar alınamadı.'); const reg = await navigator.serviceWorker.ready; const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(keyData.publicKey), }); const res = await fetch('./api/push.php', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'subscribe', subscription: sub.toJSON() }), }); const data = await res.json(); if (!data.ok) throw new Error(data.error); setPushSubscribed(true); setPushMsg({ ok: true, text: 'Push bildirimleri açıldı.' }); } catch (err) { setPushMsg({ ok: false, text: err.message || 'Etkinleştirilemedi.' }); } finally { setPushBusy(false); setTimeout(() => setPushMsg(null), 3000); } }; const disablePush = async () => { setPushBusy(true); setPushMsg(null); try { const reg = await navigator.serviceWorker.ready; const sub = await reg.pushManager.getSubscription(); if (sub) { await fetch('./api/push.php', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'unsubscribe', endpoint: sub.endpoint }), }); await sub.unsubscribe(); } setPushSubscribed(false); setPushMsg({ ok: true, text: 'Push bildirimleri kapatıldı.' }); } catch (err) { setPushMsg({ ok: false, text: err.message || 'Kapatılamadı.' }); } finally { setPushBusy(false); setTimeout(() => setPushMsg(null), 3000); } }; const sendTestPush = async () => { setPushBusy(true); setPushMsg(null); try { const res = await fetch('./api/push.php', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'test' }), }); const data = await res.json(); if (!data.ok) throw new Error(data.error); setPushMsg({ ok: data.sent > 0, text: data.sent > 0 ? 'Deneme bildirimi gönderildi.' : 'Aktif abonelik bulunamadı.' }); } catch (err) { setPushMsg({ ok: false, text: err.message || 'Gönderilemedi.' }); } finally { setPushBusy(false); setTimeout(() => setPushMsg(null), 3000); } }; const initials = (user?.name || 'U').split(' ').filter(Boolean) .map(w => w[0]).join('').slice(0, 2).toUpperCase(); // Stats const allTasks = (state?.tasks || []).filter(t => !t.archived); const doneTasks = allTasks.filter(t => t.status === 'done'); const activeProjs = (state?.projects || []).filter(p => !p.archived).length; const streak = calcStreak(allTasks); const monthStart = new Date(new Date().getFullYear(), new Date().getMonth(), 1); const doneThisMonth = doneTasks.filter(t => { const d = t.completedAt ? new Date(t.completedAt) : t.updatedAt ? new Date(t.updatedAt) : null; return d && d >= monthStart; }).length; const completionRate = allTasks.length ? Math.round(doneTasks.length / allTasks.length * 100) : 0; // Handlers const saveProfile = async () => { const n = name.trim(), e = email.trim(); if (!n) return; setProfileBusy(true); setProfileMsg(null); try { const res = await fetch('./api/profile.php?action=update_profile', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: n, email: e, title: title.trim(), phone: phone.trim(), bio: bio.trim() }), }); const data = await res.json(); if (!data.ok) throw new Error(data.error); onUpdateUser({ name: data.user.name, email: data.user.email, title: data.user.title, phone: data.user.phone, bio: data.user.bio }); setProfileMsg({ ok: true, text: 'Profil güncellendi.' }); } catch (err) { setProfileMsg({ ok: false, text: err.message || 'Güncelleme başarısız.' }); } finally { setProfileBusy(false); } }; const changePassword = async () => { if (!curPw || !newPw) return; if (newPw !== confPw) { setPwMsg({ ok: false, text: 'Şifreler eşleşmiyor.' }); return; } if (newPw.length < 8) { setPwMsg({ ok: false, text: 'En az 8 karakter gerekli.' }); return; } setPwBusy(true); setPwMsg(null); try { const res = await fetch('./api/profile.php?action=change_password', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ currentPassword: curPw, newPassword: newPw }), }); const data = await res.json(); if (!data.ok) throw new Error(data.error); setCurPw(''); setNewPw(''); setConfPw(''); setPwMsg({ ok: true, text: 'Şifre başarıyla değiştirildi.' }); } catch (err) { setPwMsg({ ok: false, text: err.message || 'Şifre değiştirilemedi.' }); } finally { setPwBusy(false); } }; const uploadAvatar = async (file) => { if (!file) return; if (file.size > 2 * 1024 * 1024) { alert('Dosya 2 MB\'ı geçemez.'); return; } setAvatarBusy(true); const fd = new FormData(); fd.append('avatar', file); try { const res = await fetch('./api/profile.php?action=upload_avatar', { method: 'POST', credentials: 'include', body: fd }); const data = await res.json(); if (!data.ok) throw new Error(data.error); const url = data.avatarUrl + '?t=' + Date.now(); setAvatarUrl(url); onUpdateUser({ avatarUrl: data.avatarUrl }); } catch (err) { alert(err.message || 'Avatar yüklenemedi.'); } finally { setAvatarBusy(false); if (fileRef.current) fileRef.current.value = ''; } }; const deleteAvatar = async () => { setAvatarBusy(true); try { const res = await fetch('./api/profile.php?action=delete_avatar', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: '{}' }); const data = await res.json(); if (!data.ok) throw new Error(data.error); setAvatarUrl(null); onUpdateUser({ avatarUrl: null }); } catch (err) { alert(err.message || 'Avatar kaldırılamadı.'); } finally { setAvatarBusy(false); } }; const saveSettings = async (dpv) => { const view = dpv || defaultView; try { await fetch('./api/profile.php?action=update_settings', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ default_project_view: view }), }); onUpdateUser({ defaultProjectView: view }); setSettingsSaved(true); setTimeout(() => setSettingsSaved(false), 2000); } catch (_) {} }; const saveDigest = async () => { setDigestBusy(true); setDigestMsg(null); try { const res = await fetch('./api/profile.php?action=update_digest', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: digestEnabled, hour: digestHour, includeOverdue: digestIncludeOverdue, projectKeys: digestProjects, }), }); const data = await res.json(); if (!data.ok) throw new Error(data.error); setDigestMsg({ ok: true, text: 'Bildirim ayarları kaydedildi.' }); } catch (err) { setDigestMsg({ ok: false, text: err.message || 'Kaydedilemedi.' }); } finally { setDigestBusy(false); setTimeout(() => setDigestMsg(null), 3000); } }; const exportJSON = () => { try { const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `navia-yedek-${new Date().toISOString().split('T')[0]}.json`; a.click(); URL.revokeObjectURL(url); setExportMsg({ ok: true, text: 'JSON yedeği indirildi.' }); } catch (_) { setExportMsg({ ok: false, text: 'İndirme başarısız.' }); } setTimeout(() => setExportMsg(null), 3000); }; const exportCSV = () => { try { const projs = Object.fromEntries((state?.projects || []).map(p => [p.id, p.name])); const STATUS_TR = { todo: 'Yapılacak', doing: 'Devam Ediyor', review: 'İncelemede', done: 'Tamamlandı' }; const PRIO_TR = { high: 'Yüksek', medium: 'Orta', low: 'Düşük' }; const esc = v => `"${(v || '').replace(/"/g, '""')}"`; const headers = ['Başlık', 'Firma', 'Durum', 'Öncelik', 'Teslim Tarihi', 'Oluşturulma']; const rows = (state?.tasks || []).filter(t => !t.archived).map(t => [ esc(t.title), esc(projs[t.projectId] || ''), STATUS_TR[t.status] || t.status, PRIO_TR[t.priority] || t.priority, t.due || '', t.createdAt ? t.createdAt.split('T')[0] : '', ]); const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\n'); const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `navia-gorevler-${new Date().toISOString().split('T')[0]}.csv`; a.click(); URL.revokeObjectURL(url); setExportMsg({ ok: true, text: 'CSV indirildi.' }); } catch (_) { setExportMsg({ ok: false, text: 'İndirme başarısız.' }); } setTimeout(() => setExportMsg(null), 3000); }; const handleImport = (file) => { if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const data = JSON.parse(e.target.result); if (!data.projects || !data.tasks) throw new Error('Geçersiz yedek dosyası.'); if (window.confirm(`${data.projects.length} firma ve ${data.tasks.length} görev geri yüklenecek. Emin misiniz?`)) { onReplaceState?.(data); setExportMsg({ ok: true, text: 'Yedek başarıyla geri yüklendi.' }); setTimeout(() => setExportMsg(null), 3000); } } catch (err) { alert(err.message || 'Geçersiz dosya.'); } }; reader.readAsText(file); if (importRef.current) importRef.current.value = ''; }; return (
{/* ── Üst başlık ─────────────────────────────── */}
{/* Avatar + isim */}
!avatarBusy && fileRef.current?.click()} title="Fotoğraf değiştir" > {avatarUrl ? {name} :
{initials}
}
{avatarBusy ? : }
uploadAvatar(e.target.files?.[0])} />
{name || 'Kullanıcı'}
{title &&
{title}
}
{email}
{avatarUrl && ( )}
{/* Stat chips */}
{doneTasks.length} tamamlandı
{activeProjs} firma
{doneThisMonth} bu ay
{completionRate}% oran
{streak > 0 && (
{streak} günlük seri 🔥
)}
{/* ── Ana içerik ─────────────────────────────── */}
{/* SOL SÜTUN */}
{/* Kişisel bilgiler */}

Kişisel Bilgiler

{ setName(e.target.value); setProfileMsg(null); }} placeholder="Adınız Soyadınız" />
{ setEmail(e.target.value); setProfileMsg(null); }} placeholder="email@domain.com" />
{ setTitle(e.target.value); setProfileMsg(null); }} placeholder="Grafik Tasarımcı…" />
{ setPhone(e.target.value); setProfileMsg(null); }} placeholder="+90 555 000 00 00" />