MAJOR FEATURES: - AI Control Tab in Settings hinzugefügt mit vollständiger KI-Steuerung - Category Control: Benutzer können Kategorien aktivieren/deaktivieren und Aktionen pro Kategorie festlegen (Keep in Inbox, Archive & Mark Read, Star) - Company Labels: Automatische Erkennung bekannter Firmen (Amazon, Google, Microsoft, etc.) und optionale benutzerdefinierte Company Labels - Auto-Detect Companies Toggle: Automatische Label-Erstellung für bekannte Firmen UI/UX VERBESSERUNGEN: - Sorting Rules Tab entfernt (war zu verwirrend) - Save Buttons nach oben rechts verschoben (Category Control und Company Labels) - Company Labels Section: Custom Labels sind jetzt in einem ausklappbaren Details-Element (Optional) - Verbesserte Beschreibungen und Klarheit in der UI BACKEND ÄNDERUNGEN: - Neue API Endpoints: /api/preferences/ai-control (GET/POST) und /api/preferences/company-labels (GET/POST/DELETE) - AI Sorter Service erweitert: detectCompany(), matchesCompanyLabel(), getCategoryAction(), getEnabledCategories() - Database Service: Default-Werte und Merge-Logik für erweiterte User Preferences - Email Routes: Integration der neuen AI Control Einstellungen in Gmail und Outlook Sortierung - Label-Erstellung: Nur für enabledCategories, Custom Company Labels mit orange Farbe (#ff9800) FRONTEND ÄNDERUNGEN: - Neue TypeScript Types: client/src/types/settings.ts (AIControlSettings, CompanyLabel, CategoryInfo, KnownCompany) - Settings.tsx: Komplett überarbeitet mit AI Control Tab, Category Toggles, Company Labels Management - API Client erweitert: getAIControlSettings(), saveAIControlSettings(), getCompanyLabels(), saveCompanyLabel(), deleteCompanyLabel() - Debug-Logs hinzugefügt für Troubleshooting (main.tsx, App.tsx, Settings.tsx) BUGFIXES: - JSX Syntax-Fehler behoben: Fehlende schließende </div> Tags in Company Labels Section - TypeScript Typ-Fehler behoben: saved.data null-check für Company Labels - Struktur-Fehler behoben: Conditional Blocks korrekt verschachtelt TECHNISCHE DETAILS: - 9 Kategorien verfügbar: VIP, Clients, Invoices, Newsletter, Promotions, Social, Security, Calendar, Review - Company Labels unterstützen Bedingungen wie 'from:amazon.com OR from:amazon.de' - Priorisierung: 1) Custom Company Labels, 2) Auto-Detected Companies, 3) AI Categorization - Deaktivierte Kategorien werden automatisch als 'review' kategorisiert
943 lines
48 KiB
TypeScript
943 lines
48 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
|
import { useAuth } from '@/context/AuthContext'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Label } from '@/components/ui/label'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { api } from '@/lib/api'
|
|
import {
|
|
Mail,
|
|
User,
|
|
CreditCard,
|
|
Shield,
|
|
Settings as SettingsIcon,
|
|
ArrowLeft,
|
|
Plus,
|
|
Trash2,
|
|
Check,
|
|
X,
|
|
ExternalLink,
|
|
Loader2,
|
|
Crown,
|
|
Star,
|
|
Brain,
|
|
Building2,
|
|
} from 'lucide-react'
|
|
import type { AIControlSettings, CompanyLabel, CategoryInfo } from '@/types/settings'
|
|
|
|
type TabType = 'profile' | 'accounts' | 'vip' | 'ai-control' | 'subscription'
|
|
|
|
interface EmailAccount {
|
|
id: string
|
|
email: string
|
|
provider: 'gmail' | 'outlook'
|
|
connected: boolean
|
|
lastSync?: string
|
|
}
|
|
|
|
interface VIPSender {
|
|
email: string
|
|
name?: string
|
|
}
|
|
|
|
interface Subscription {
|
|
status: string
|
|
plan: string
|
|
currentPeriodEnd?: string
|
|
cancelAtPeriodEnd?: boolean
|
|
}
|
|
|
|
export function Settings() {
|
|
// #region agent log
|
|
try {
|
|
fetch('http://127.0.0.1:7242/ingest/4fa7412d-6f79-4871-8728-29c37c9e5772',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'Settings.tsx:52',message:'Settings component rendering',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'A'})}).catch(()=>{});
|
|
} catch(e) {}
|
|
// #endregion
|
|
|
|
const { user } = useAuth()
|
|
const navigate = useNavigate()
|
|
const [searchParams, setSearchParams] = useSearchParams()
|
|
|
|
const activeTab = (searchParams.get('tab') as TabType) || 'profile'
|
|
|
|
// #region agent log
|
|
try {
|
|
fetch('http://127.0.0.1:7242/ingest/4fa7412d-6f79-4871-8728-29c37c9e5772',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'Settings.tsx:60',message:'Settings state initialized',data:{hasUser:!!user,activeTab},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'A'})}).catch(()=>{});
|
|
} catch(e) {}
|
|
// #endregion
|
|
const [loading, setLoading] = useState(false)
|
|
const [saving, setSaving] = useState(false)
|
|
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
|
|
|
|
const [name, setName] = useState(user?.name || '')
|
|
const [email] = useState(user?.email || '')
|
|
const [accounts, setAccounts] = useState<EmailAccount[]>([])
|
|
const [connectingProvider, setConnectingProvider] = useState<string | null>(null)
|
|
const [vipSenders, setVipSenders] = useState<VIPSender[]>([])
|
|
const [newVipEmail, setNewVipEmail] = useState('')
|
|
const [subscription, setSubscription] = useState<Subscription | null>(null)
|
|
|
|
// AI Control state
|
|
const [aiControlSettings, setAiControlSettings] = useState<AIControlSettings>({
|
|
enabledCategories: [],
|
|
categoryActions: {},
|
|
autoDetectCompanies: true,
|
|
})
|
|
const [categories, setCategories] = useState<CategoryInfo[]>([])
|
|
const [companyLabels, setCompanyLabels] = useState<CompanyLabel[]>([])
|
|
const [newCompanyLabel, setNewCompanyLabel] = useState({ name: '', condition: '', category: 'promotions' })
|
|
|
|
useEffect(() => {
|
|
loadData()
|
|
}, [user])
|
|
|
|
const loadData = async () => {
|
|
// #region agent log
|
|
try {
|
|
fetch('http://127.0.0.1:7242/ingest/4fa7412d-6f79-4871-8728-29c37c9e5772',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'Settings.tsx:84',message:'loadData called',data:{hasUser:!!user,userId:user?.$id},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'E'})}).catch(()=>{});
|
|
} catch(e) {}
|
|
// #endregion
|
|
|
|
if (!user?.$id) return
|
|
setLoading(true)
|
|
|
|
try {
|
|
const [accountsRes, subsRes, prefsRes, aiControlRes, companyLabelsRes] = await Promise.all([
|
|
api.getEmailAccounts(user.$id),
|
|
api.getSubscriptionStatus(user.$id),
|
|
api.getUserPreferences(user.$id),
|
|
api.getAIControlSettings(user.$id),
|
|
api.getCompanyLabels(user.$id),
|
|
])
|
|
|
|
if (accountsRes.data) setAccounts(accountsRes.data)
|
|
if (subsRes.data) setSubscription(subsRes.data)
|
|
if (prefsRes.data?.vipSenders) setVipSenders(prefsRes.data.vipSenders)
|
|
if (aiControlRes.data) setAiControlSettings(aiControlRes.data)
|
|
if (companyLabelsRes.data) setCompanyLabels(companyLabelsRes.data)
|
|
|
|
// Load categories from API or use defaults
|
|
const categoryList: CategoryInfo[] = [
|
|
{ key: 'vip', name: 'Important', description: 'Important emails from known contacts', defaultAction: 'star', color: '#ff0000', enabled: true },
|
|
{ key: 'customers', name: 'Clients', description: 'Emails from clients and projects', defaultAction: 'inbox', color: '#4285f4', enabled: true },
|
|
{ key: 'invoices', name: 'Invoices', description: 'Invoices, receipts and financial documents', defaultAction: 'inbox', color: '#0f9d58', enabled: true },
|
|
{ key: 'newsletters', name: 'Newsletter', description: 'Regular newsletters and updates', defaultAction: 'archive_read', color: '#9c27b0', enabled: true },
|
|
{ key: 'promotions', name: 'Promotions', description: 'Marketing emails and promotions', defaultAction: 'archive_read', color: '#ff9800', enabled: true },
|
|
{ key: 'social', name: 'Social', description: 'Social media and platform notifications', defaultAction: 'archive_read', color: '#00bcd4', enabled: true },
|
|
{ key: 'security', name: 'Security', description: 'Security codes and notifications', defaultAction: 'inbox', color: '#f44336', enabled: true },
|
|
{ key: 'calendar', name: 'Calendar', description: 'Calendar invites and events', defaultAction: 'inbox', color: '#673ab7', enabled: true },
|
|
{ key: 'review', name: 'Review', description: 'Emails that need manual review', defaultAction: 'inbox', color: '#607d8b', enabled: true },
|
|
]
|
|
|
|
// Update enabled status from settings
|
|
const enabledCategories = aiControlRes.data?.enabledCategories || categoryList.map(c => c.key)
|
|
const updatedCategories = categoryList.map(cat => ({
|
|
...cat,
|
|
enabled: enabledCategories.includes(cat.key),
|
|
}))
|
|
setCategories(updatedCategories)
|
|
|
|
// #region agent log
|
|
try {
|
|
fetch('http://127.0.0.1:7242/ingest/4fa7412d-6f79-4871-8728-29c37c9e5772',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'Settings.tsx:122',message:'loadData success',data:{accountsCount:accountsRes.data?.length||0,categoriesCount:updatedCategories.length},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'E'})}).catch(()=>{});
|
|
} catch(e) {}
|
|
// #endregion
|
|
} catch (error) {
|
|
// #region agent log
|
|
try {
|
|
fetch('http://127.0.0.1:7242/ingest/4fa7412d-6f79-4871-8728-29c37c9e5772',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'Settings.tsx:123',message:'loadData error',data:{errorMessage:error instanceof Error?error.message:String(error),errorStack:error instanceof Error?error.stack:undefined},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'E'})}).catch(()=>{});
|
|
} catch(e) {}
|
|
// #endregion
|
|
console.error('Failed to load settings data:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const setTab = (tab: TabType) => {
|
|
setSearchParams({ tab })
|
|
setMessage(null)
|
|
}
|
|
|
|
const showMessage = (type: 'success' | 'error', text: string) => {
|
|
setMessage({ type, text })
|
|
setTimeout(() => setMessage(null), 5000)
|
|
}
|
|
|
|
const handleSaveProfile = async () => {
|
|
setSaving(true)
|
|
try {
|
|
showMessage('success', 'Profile saved!')
|
|
} catch {
|
|
showMessage('error', 'Failed to save')
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
const handleConnectAccount = async (provider: 'gmail' | 'outlook') => {
|
|
if (!user?.$id) return
|
|
setConnectingProvider(provider)
|
|
|
|
try {
|
|
const res = await api.getOAuthUrl(provider, user.$id)
|
|
if (res.data?.url) {
|
|
window.location.href = res.data.url
|
|
}
|
|
} catch {
|
|
showMessage('error', `Failed to connect ${provider}`)
|
|
setConnectingProvider(null)
|
|
}
|
|
}
|
|
|
|
const handleDisconnectAccount = async (accountId: string) => {
|
|
if (!user?.$id) return
|
|
|
|
try {
|
|
await api.disconnectEmailAccount(accountId, user.$id)
|
|
setAccounts(accounts.filter(a => a.id !== accountId))
|
|
showMessage('success', 'Account disconnected')
|
|
} catch {
|
|
showMessage('error', 'Failed to disconnect')
|
|
}
|
|
}
|
|
|
|
const handleAddVip = () => {
|
|
if (!newVipEmail.trim() || !newVipEmail.includes('@')) return
|
|
|
|
if (vipSenders.some(v => v.email === newVipEmail)) {
|
|
showMessage('error', 'This email is already in the VIP list')
|
|
return
|
|
}
|
|
|
|
setVipSenders([...vipSenders, { email: newVipEmail }])
|
|
setNewVipEmail('')
|
|
showMessage('success', 'VIP added')
|
|
}
|
|
|
|
const handleRemoveVip = (email: string) => {
|
|
setVipSenders(vipSenders.filter(v => v.email !== email))
|
|
}
|
|
|
|
const handleSaveVips = async () => {
|
|
if (!user?.$id) return
|
|
setSaving(true)
|
|
|
|
try {
|
|
await api.saveUserPreferences(user.$id, { vipSenders })
|
|
showMessage('success', 'VIP list saved!')
|
|
} catch {
|
|
showMessage('error', 'Failed to save')
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
const handleManageSubscription = async () => {
|
|
if (!user?.$id) return
|
|
|
|
try {
|
|
const res = await api.createPortalSession(user.$id)
|
|
if (res.data?.url) {
|
|
window.location.href = res.data.url
|
|
}
|
|
} catch {
|
|
showMessage('error', 'Failed to open customer portal')
|
|
}
|
|
}
|
|
|
|
const handleUpgrade = async (plan: string) => {
|
|
if (!user?.$id) return
|
|
|
|
try {
|
|
const res = await api.createSubscriptionCheckout(plan, user.$id, user.email)
|
|
if (res.data?.url) {
|
|
window.location.href = res.data.url
|
|
}
|
|
} catch {
|
|
showMessage('error', 'Failed to start checkout')
|
|
}
|
|
}
|
|
|
|
const tabs = [
|
|
{ id: 'profile' as TabType, label: 'Profile', icon: User },
|
|
{ id: 'accounts' as TabType, label: 'Email Accounts', icon: Mail },
|
|
{ id: 'vip' as TabType, label: 'VIP List', icon: Star },
|
|
{ id: 'ai-control' as TabType, label: 'AI Control', icon: Brain },
|
|
{ id: 'subscription' as TabType, label: 'Subscription', icon: CreditCard },
|
|
]
|
|
|
|
// #region agent log
|
|
try {
|
|
fetch('http://127.0.0.1:7242/ingest/4fa7412d-6f79-4871-8728-29c37c9e5772',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'Settings.tsx:243',message:'Settings render starting',data:{loading,activeTab,hasUser:!!user},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'A'})}).catch(()=>{});
|
|
} catch(e) {}
|
|
// #endregion
|
|
|
|
return (
|
|
<div className="min-h-screen bg-slate-50">
|
|
<header className="bg-white border-b border-slate-200 sticky top-0 z-40">
|
|
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="flex items-center h-16">
|
|
<Button variant="ghost" onClick={() => navigate('/dashboard')} className="mr-4">
|
|
<ArrowLeft className="w-5 h-5 mr-2" />
|
|
Back
|
|
</Button>
|
|
<div className="flex items-center gap-2">
|
|
<SettingsIcon className="w-5 h-5 text-slate-500" />
|
|
<h1 className="text-lg font-semibold text-slate-900">Settings</h1>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
{message && (
|
|
<div className={`mb-6 p-4 rounded-lg flex items-center gap-2 ${
|
|
message.type === 'success'
|
|
? 'bg-green-50 text-green-700 border border-green-200'
|
|
: 'bg-red-50 text-red-700 border border-red-200'
|
|
}`}>
|
|
{message.type === 'success' ? <Check className="w-5 h-5" /> : <X className="w-5 h-5" />}
|
|
{message.text}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-col lg:flex-row gap-8">
|
|
<nav className="lg:w-64 flex-shrink-0">
|
|
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
|
{tabs.map((tab) => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setTab(tab.id)}
|
|
className={`w-full flex items-center gap-3 px-4 py-3 text-left transition-colors ${
|
|
activeTab === tab.id
|
|
? 'bg-primary-50 text-primary-700 border-l-4 border-primary-500'
|
|
: 'text-slate-600 hover:bg-slate-50 border-l-4 border-transparent'
|
|
}`}
|
|
>
|
|
<tab.icon className="w-5 h-5" />
|
|
<span className="font-medium">{tab.label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</nav>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="w-8 h-8 animate-spin text-primary-500" />
|
|
</div>
|
|
) : (
|
|
<>
|
|
{activeTab === 'profile' && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Profile</CardTitle>
|
|
<CardDescription>Manage your personal information</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center text-white text-2xl font-bold">
|
|
{name?.charAt(0)?.toUpperCase() || email?.charAt(0)?.toUpperCase() || 'U'}
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-slate-900">{name || 'User'}</h3>
|
|
<p className="text-slate-500">{email}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="name">Name</Label>
|
|
<Input
|
|
id="name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Your name"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="email">Email</Label>
|
|
<Input id="email" value={email} disabled className="bg-slate-50" />
|
|
<p className="text-xs text-slate-500 mt-1">Email address cannot be changed</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Button onClick={handleSaveProfile} disabled={saving}>
|
|
{saving ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : null}
|
|
Save
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{activeTab === 'accounts' && (
|
|
<div className="space-y-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Connected Email Accounts</CardTitle>
|
|
<CardDescription>Connect your email accounts for automatic sorting</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{accounts.length > 0 ? (
|
|
accounts.map((account) => (
|
|
<div key={account.id} className="flex items-center justify-between p-4 bg-slate-50 rounded-lg">
|
|
<div className="flex items-center gap-4">
|
|
<div className={`w-12 h-12 rounded-lg flex items-center justify-center ${
|
|
account.provider === 'gmail' ? 'bg-red-100' : 'bg-blue-100'
|
|
}`}>
|
|
<Mail className={`w-6 h-6 ${account.provider === 'gmail' ? 'text-red-600' : 'text-blue-600'}`} />
|
|
</div>
|
|
<div>
|
|
<p className="font-medium text-slate-900">{account.email}</p>
|
|
<p className="text-sm text-slate-500 capitalize">{account.provider}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Badge variant={account.connected ? 'success' : 'secondary'}>
|
|
{account.connected ? 'Connected' : 'Disconnected'}
|
|
</Badge>
|
|
<Button variant="ghost" size="icon" onClick={() => handleDisconnectAccount(account.id)}>
|
|
<Trash2 className="w-4 h-4 text-red-500" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
<p className="text-center text-slate-500 py-8">No email accounts connected yet</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Add Account</CardTitle>
|
|
<CardDescription>Connect a new email account</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid sm:grid-cols-2 gap-4">
|
|
<button
|
|
onClick={() => handleConnectAccount('gmail')}
|
|
disabled={connectingProvider === 'gmail'}
|
|
className="flex items-center gap-4 p-4 border-2 border-slate-200 rounded-xl hover:border-red-300 hover:bg-red-50 transition-all disabled:opacity-50"
|
|
>
|
|
{connectingProvider === 'gmail' ? (
|
|
<Loader2 className="w-8 h-8 animate-spin text-red-500" />
|
|
) : (
|
|
<div className="w-12 h-12 rounded-lg bg-red-100 flex items-center justify-center">
|
|
<svg className="w-6 h-6" viewBox="0 0 24 24">
|
|
<path fill="#EA4335" d="M12 11.3L1.5 3.5h21z"/>
|
|
<path fill="#34A853" d="M12 12.7L1.5 20.5V3.5z"/>
|
|
<path fill="#FBBC05" d="M1.5 20.5h21v-17z"/>
|
|
<path fill="#4285F4" d="M22.5 3.5v17L12 12.7z"/>
|
|
</svg>
|
|
</div>
|
|
)}
|
|
<div className="text-left">
|
|
<p className="font-semibold text-slate-900">Gmail</p>
|
|
<p className="text-sm text-slate-500">Connect Google account</p>
|
|
</div>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleConnectAccount('outlook')}
|
|
disabled={connectingProvider === 'outlook'}
|
|
className="flex items-center gap-4 p-4 border-2 border-slate-200 rounded-xl hover:border-blue-300 hover:bg-blue-50 transition-all disabled:opacity-50"
|
|
>
|
|
{connectingProvider === 'outlook' ? (
|
|
<Loader2 className="w-8 h-8 animate-spin text-blue-500" />
|
|
) : (
|
|
<div className="w-12 h-12 rounded-lg bg-blue-100 flex items-center justify-center">
|
|
<svg className="w-6 h-6" viewBox="0 0 24 24">
|
|
<path fill="#0078D4" d="M2 6.5v11c0 .83.67 1.5 1.5 1.5h9c.83 0 1.5-.67 1.5-1.5v-11c0-.83-.67-1.5-1.5-1.5h-9C2.67 5 2 5.67 2 6.5z"/>
|
|
<path fill="#0078D4" d="M14 6v12l8-6z"/>
|
|
</svg>
|
|
</div>
|
|
)}
|
|
<div className="text-left">
|
|
<p className="font-semibold text-slate-900">Outlook</p>
|
|
<p className="text-sm text-slate-500">Connect Microsoft account</p>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'vip' && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Shield className="w-5 h-5 text-amber-500" />
|
|
VIP List
|
|
</CardTitle>
|
|
<CardDescription>Emails from these senders will always be marked as important</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="flex gap-2">
|
|
<Input
|
|
placeholder="email@example.com"
|
|
value={newVipEmail}
|
|
onChange={(e) => setNewVipEmail(e.target.value)}
|
|
onKeyDown={(e) => e.key === 'Enter' && handleAddVip()}
|
|
/>
|
|
<Button onClick={handleAddVip}>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Add
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
{vipSenders.length > 0 ? (
|
|
vipSenders.map((vip) => (
|
|
<div key={vip.email} className="flex items-center justify-between p-3 bg-amber-50 border border-amber-100 rounded-lg">
|
|
<div className="flex items-center gap-3">
|
|
<Star className="w-5 h-5 text-amber-500" />
|
|
<span className="text-slate-700">{vip.email}</span>
|
|
</div>
|
|
<Button variant="ghost" size="icon" onClick={() => handleRemoveVip(vip.email)}>
|
|
<X className="w-4 h-4 text-slate-400 hover:text-red-500" />
|
|
</Button>
|
|
</div>
|
|
))
|
|
) : (
|
|
<p className="text-center text-slate-500 py-8">No VIP senders added yet</p>
|
|
)}
|
|
</div>
|
|
|
|
{vipSenders.length > 0 && (
|
|
<Button onClick={handleSaveVips} disabled={saving}>
|
|
{saving ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : null}
|
|
Save changes
|
|
</Button>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{activeTab === 'ai-control' && (
|
|
<div className="space-y-6">
|
|
{/* Category Toggles */}
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Brain className="w-5 h-5 text-primary-500" />
|
|
Category Control
|
|
</CardTitle>
|
|
<CardDescription>Enable or disable email categories for AI sorting</CardDescription>
|
|
</div>
|
|
<Button
|
|
onClick={async () => {
|
|
if (!user?.$id) return
|
|
setSaving(true)
|
|
try {
|
|
await api.saveAIControlSettings(user.$id, {
|
|
enabledCategories: aiControlSettings.enabledCategories,
|
|
categoryActions: aiControlSettings.categoryActions,
|
|
autoDetectCompanies: aiControlSettings.autoDetectCompanies,
|
|
})
|
|
showMessage('success', 'AI Control settings saved!')
|
|
} catch {
|
|
showMessage('error', 'Failed to save settings')
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}}
|
|
disabled={saving}
|
|
size="sm"
|
|
>
|
|
{saving ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Check className="w-4 h-4 mr-2" />}
|
|
Save
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{categories.map((category) => (
|
|
<div key={category.key} className="flex items-center justify-between p-4 rounded-lg border border-slate-200 bg-white">
|
|
<div className="flex items-center gap-4 flex-1">
|
|
<button
|
|
onClick={() => {
|
|
const newEnabled = category.enabled
|
|
? aiControlSettings.enabledCategories.filter(c => c !== category.key)
|
|
: [...aiControlSettings.enabledCategories, category.key]
|
|
setAiControlSettings({ ...aiControlSettings, enabledCategories: newEnabled })
|
|
setCategories(categories.map(c => c.key === category.key ? { ...c, enabled: !c.enabled } : c))
|
|
}}
|
|
className={`w-10 h-6 rounded-full transition-colors ${category.enabled ? 'bg-primary-500' : 'bg-slate-300'}`}
|
|
>
|
|
<div className={`w-4 h-4 bg-white rounded-full transform transition-transform mx-1 ${
|
|
category.enabled ? 'translate-x-4' : 'translate-x-0'
|
|
}`} />
|
|
</button>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-4 h-4 rounded" style={{ backgroundColor: category.color }} />
|
|
<p className={`font-medium ${category.enabled ? 'text-slate-900' : 'text-slate-500'}`}>
|
|
{category.name}
|
|
</p>
|
|
</div>
|
|
<p className="text-sm text-slate-500">{category.description}</p>
|
|
</div>
|
|
</div>
|
|
{category.enabled && (
|
|
<select
|
|
value={aiControlSettings.categoryActions[category.key] || category.defaultAction}
|
|
onChange={(e) => {
|
|
const newActions = { ...aiControlSettings.categoryActions, [category.key]: e.target.value as any }
|
|
setAiControlSettings({ ...aiControlSettings, categoryActions: newActions })
|
|
}}
|
|
className="px-3 py-1.5 text-sm border border-slate-300 rounded-lg bg-white"
|
|
>
|
|
<option value="inbox">Keep in Inbox</option>
|
|
<option value="archive_read">Archive & Mark Read</option>
|
|
<option value="star">Star</option>
|
|
</select>
|
|
)}
|
|
</div>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Company Labels */}
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Building2 className="w-5 h-5 text-primary-500" />
|
|
Company Labels
|
|
</CardTitle>
|
|
<CardDescription>Automatically label emails from specific companies</CardDescription>
|
|
</div>
|
|
<Button
|
|
onClick={async () => {
|
|
if (!user?.$id) return
|
|
setSaving(true)
|
|
try {
|
|
await api.saveAIControlSettings(user.$id, {
|
|
autoDetectCompanies: aiControlSettings.autoDetectCompanies,
|
|
})
|
|
showMessage('success', 'Settings saved!')
|
|
} catch {
|
|
showMessage('error', 'Failed to save')
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}}
|
|
disabled={saving}
|
|
size="sm"
|
|
>
|
|
{saving ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Check className="w-4 h-4 mr-2" />}
|
|
Save
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
{/* Auto-Detection Toggle */}
|
|
<div className="flex items-center justify-between p-4 rounded-lg border border-slate-200 bg-white">
|
|
<div>
|
|
<p className="font-medium text-slate-900">Auto-Detect Known Companies</p>
|
|
<p className="text-sm text-slate-500">Automatically detect and label emails from Amazon, Google, Microsoft, etc. Labels are created automatically when emails from these companies are detected.</p>
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
const newValue = !aiControlSettings.autoDetectCompanies
|
|
setAiControlSettings({ ...aiControlSettings, autoDetectCompanies: newValue })
|
|
}}
|
|
className={`w-10 h-6 rounded-full transition-colors ${aiControlSettings.autoDetectCompanies ? 'bg-primary-500' : 'bg-slate-300'}`}
|
|
>
|
|
<div className={`w-4 h-4 bg-white rounded-full transform transition-transform mx-1 ${
|
|
aiControlSettings.autoDetectCompanies ? 'translate-x-4' : 'translate-x-0'
|
|
}`} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Custom Company Labels - Optional */}
|
|
{companyLabels.length > 0 && (
|
|
<div className="space-y-4">
|
|
<h3 className="font-semibold text-slate-900">Custom Company Labels (Optional)</h3>
|
|
|
|
{/* Add New Label Form */}
|
|
<div className="p-4 rounded-lg border border-slate-200 bg-slate-50 space-y-3">
|
|
<div>
|
|
<Label htmlFor="label-name">Company Name</Label>
|
|
<Input
|
|
id="label-name"
|
|
placeholder="e.g., Amazon"
|
|
value={newCompanyLabel.name}
|
|
onChange={(e) => setNewCompanyLabel({ ...newCompanyLabel, name: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="label-condition">Condition</Label>
|
|
<Input
|
|
id="label-condition"
|
|
placeholder="e.g., from:amazon.com OR from:amazon.de"
|
|
value={newCompanyLabel.condition}
|
|
onChange={(e) => setNewCompanyLabel({ ...newCompanyLabel, condition: e.target.value })}
|
|
/>
|
|
<p className="text-xs text-slate-500 mt-1">Use "from:domain.com" or "subject:keyword"</p>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="label-category">Category</Label>
|
|
<select
|
|
id="label-category"
|
|
value={newCompanyLabel.category}
|
|
onChange={(e) => setNewCompanyLabel({ ...newCompanyLabel, category: e.target.value })}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg bg-white"
|
|
>
|
|
<option value="promotions">Promotions</option>
|
|
<option value="newsletters">Newsletter</option>
|
|
<option value="invoices">Invoices</option>
|
|
<option value="customers">Clients</option>
|
|
</select>
|
|
</div>
|
|
<Button
|
|
onClick={async () => {
|
|
if (!user?.$id || !newCompanyLabel.name || !newCompanyLabel.condition) return
|
|
try {
|
|
const saved = await api.saveCompanyLabel(user.$id, {
|
|
...newCompanyLabel,
|
|
enabled: true,
|
|
})
|
|
if (saved.data) {
|
|
setCompanyLabels([...companyLabels, saved.data])
|
|
setNewCompanyLabel({ name: '', condition: '', category: 'promotions' })
|
|
showMessage('success', 'Company label created!')
|
|
}
|
|
} catch {
|
|
showMessage('error', 'Failed to create label')
|
|
}
|
|
}}
|
|
className="w-full"
|
|
>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Add Company Label
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Existing Labels */}
|
|
<div className="space-y-2">
|
|
{companyLabels.map((label) => (
|
|
<div key={label.id} className="flex items-center justify-between p-4 rounded-lg border border-slate-200 bg-white">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<p className="font-medium text-slate-900">{label.name}</p>
|
|
<Badge variant={label.enabled ? 'default' : 'secondary'}>
|
|
{label.enabled ? 'Enabled' : 'Disabled'}
|
|
</Badge>
|
|
</div>
|
|
<p className="text-sm text-slate-500 font-mono mt-1">{label.condition}</p>
|
|
{label.category && (
|
|
<p className="text-xs text-slate-400 mt-1">Category: {label.category}</p>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={async () => {
|
|
if (!user?.$id || !label.id) return
|
|
try {
|
|
await api.saveCompanyLabel(user.$id, { ...label, enabled: !label.enabled })
|
|
setCompanyLabels(companyLabels.map(l => l.id === label.id ? { ...l, enabled: !l.enabled } : l))
|
|
showMessage('success', 'Label updated!')
|
|
} catch {
|
|
showMessage('error', 'Failed to update label')
|
|
}
|
|
}}
|
|
className={`w-10 h-6 rounded-full transition-colors ${label.enabled ? 'bg-primary-500' : 'bg-slate-300'}`}
|
|
>
|
|
<div className={`w-4 h-4 bg-white rounded-full transform transition-transform mx-1 ${
|
|
label.enabled ? 'translate-x-4' : 'translate-x-0'
|
|
}`} />
|
|
</button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={async () => {
|
|
if (!user?.$id || !label.id) return
|
|
try {
|
|
await api.deleteCompanyLabel(user.$id, label.id)
|
|
setCompanyLabels(companyLabels.filter(l => l.id !== label.id))
|
|
showMessage('success', 'Label deleted!')
|
|
} catch {
|
|
showMessage('error', 'Failed to delete label')
|
|
}
|
|
}}
|
|
>
|
|
<Trash2 className="w-4 h-4 text-red-500" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Add Custom Label - Collapsible */}
|
|
<details className="group">
|
|
<summary className="cursor-pointer p-4 rounded-lg border border-slate-200 bg-slate-50 hover:bg-slate-100 transition-colors">
|
|
<div className="flex items-center justify-between">
|
|
<span className="font-medium text-slate-900">Add Custom Company Label (Optional)</span>
|
|
<Plus className="w-5 h-5 text-slate-500 group-open:rotate-45 transition-transform" />
|
|
</div>
|
|
</summary>
|
|
<div className="mt-4 p-4 rounded-lg border border-slate-200 bg-white space-y-3">
|
|
<div>
|
|
<Label htmlFor="label-name">Company Name</Label>
|
|
<Input
|
|
id="label-name"
|
|
placeholder="e.g., Amazon"
|
|
value={newCompanyLabel.name}
|
|
onChange={(e) => setNewCompanyLabel({ ...newCompanyLabel, name: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="label-condition">Condition</Label>
|
|
<Input
|
|
id="label-condition"
|
|
placeholder="e.g., from:amazon.com OR from:amazon.de"
|
|
value={newCompanyLabel.condition}
|
|
onChange={(e) => setNewCompanyLabel({ ...newCompanyLabel, condition: e.target.value })}
|
|
/>
|
|
<p className="text-xs text-slate-500 mt-1">Use "from:domain.com" or "subject:keyword"</p>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="label-category">Category</Label>
|
|
<select
|
|
id="label-category"
|
|
value={newCompanyLabel.category}
|
|
onChange={(e) => setNewCompanyLabel({ ...newCompanyLabel, category: e.target.value })}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg bg-white"
|
|
>
|
|
<option value="promotions">Promotions</option>
|
|
<option value="newsletters">Newsletter</option>
|
|
<option value="invoices">Invoices</option>
|
|
<option value="customers">Clients</option>
|
|
</select>
|
|
</div>
|
|
<Button
|
|
onClick={async () => {
|
|
if (!user?.$id || !newCompanyLabel.name || !newCompanyLabel.condition) return
|
|
try {
|
|
const saved = await api.saveCompanyLabel(user.$id, {
|
|
...newCompanyLabel,
|
|
enabled: true,
|
|
})
|
|
if (saved.data) {
|
|
setCompanyLabels([...companyLabels, saved.data])
|
|
setNewCompanyLabel({ name: '', condition: '', category: 'promotions' })
|
|
showMessage('success', 'Company label created!')
|
|
}
|
|
} catch {
|
|
showMessage('error', 'Failed to create label')
|
|
}
|
|
}}
|
|
className="w-full"
|
|
>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Add Company Label
|
|
</Button>
|
|
</div>
|
|
</details>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'subscription' && (
|
|
<div className="space-y-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Current Subscription</CardTitle>
|
|
<CardDescription>Manage your EmailSorter subscription</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex items-center justify-between p-4 bg-gradient-to-r from-primary-50 to-accent-50 rounded-xl border border-primary-100">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-14 h-14 rounded-xl bg-white shadow-sm flex items-center justify-center">
|
|
<Crown className="w-7 h-7 text-primary-500" />
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
<h3 className="font-bold text-lg text-slate-900">{subscription?.plan || 'Trial'}</h3>
|
|
<Badge variant={subscription?.status === 'active' ? 'success' : 'warning'}>
|
|
{subscription?.status === 'active' ? 'Active' : 'Trial'}
|
|
</Badge>
|
|
</div>
|
|
{subscription?.currentPeriodEnd && (
|
|
<p className="text-sm text-slate-500">
|
|
Next billing: {new Date(subscription.currentPeriodEnd).toLocaleDateString('en-US')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<Button onClick={handleManageSubscription}>
|
|
<ExternalLink className="w-4 h-4 mr-2" />
|
|
Manage
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Available Plans</CardTitle>
|
|
<CardDescription>Choose the plan that fits you</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid sm:grid-cols-3 gap-4">
|
|
{[
|
|
{ id: 'basic', name: 'Basic', price: '9', features: ['1 email account', '500 emails/day', 'Standard support'] },
|
|
{ id: 'pro', name: 'Pro', price: '19', features: ['3 email accounts', 'Unlimited emails', 'Historical sorting', 'Priority support'], popular: true },
|
|
{ id: 'business', name: 'Business', price: '49', features: ['10 email accounts', 'Unlimited emails', 'Team features', 'API access', '24/7 support'] },
|
|
].map((plan) => (
|
|
<div key={plan.id} className={`relative p-6 rounded-xl border-2 ${
|
|
plan.popular ? 'border-primary-500 bg-primary-50' : 'border-slate-200 bg-white'
|
|
}`}>
|
|
{plan.popular && (
|
|
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
|
|
<Badge className="bg-primary-500 text-white">Popular</Badge>
|
|
</div>
|
|
)}
|
|
<h3 className="font-bold text-lg text-slate-900">{plan.name}</h3>
|
|
<div className="mt-2 mb-4">
|
|
<span className="text-3xl font-bold text-slate-900">${plan.price}</span>
|
|
<span className="text-slate-500">/month</span>
|
|
</div>
|
|
<ul className="space-y-2 mb-6">
|
|
{plan.features.map((feature) => (
|
|
<li key={feature} className="flex items-center gap-2 text-sm text-slate-600">
|
|
<Check className="w-4 h-4 text-green-500" />
|
|
{feature}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<Button
|
|
className="w-full"
|
|
variant={plan.popular ? 'default' : 'outline'}
|
|
onClick={() => handleUpgrade(plan.id)}
|
|
disabled={subscription?.plan === plan.id}
|
|
>
|
|
{subscription?.plan === plan.id ? 'Current plan' : 'Select'}
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|