feat: AI Control Settings mit Category Control und Company Labels
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
This commit is contained in:
@@ -106,6 +106,105 @@ app.post('/api/preferences', asyncHandler(async (req, res) => {
|
||||
respond.success(res, null, 'Einstellungen gespeichert')
|
||||
}))
|
||||
|
||||
/**
|
||||
* GET /api/preferences/ai-control
|
||||
* Get AI Control settings
|
||||
*/
|
||||
app.get('/api/preferences/ai-control', asyncHandler(async (req, res) => {
|
||||
const { userId } = req.query
|
||||
if (!userId) throw new ValidationError('userId is required')
|
||||
|
||||
const prefs = await userPreferences.getByUser(userId)
|
||||
const preferences = prefs?.preferences || userPreferences.getDefaults()
|
||||
|
||||
respond.success(res, {
|
||||
enabledCategories: preferences.enabledCategories || [],
|
||||
categoryActions: preferences.categoryActions || {},
|
||||
autoDetectCompanies: preferences.autoDetectCompanies !== undefined ? preferences.autoDetectCompanies : true,
|
||||
})
|
||||
}))
|
||||
|
||||
/**
|
||||
* POST /api/preferences/ai-control
|
||||
* Save AI Control settings
|
||||
*/
|
||||
app.post('/api/preferences/ai-control', asyncHandler(async (req, res) => {
|
||||
const { userId, enabledCategories, categoryActions, autoDetectCompanies } = req.body
|
||||
if (!userId) throw new ValidationError('userId is required')
|
||||
|
||||
const updates = {}
|
||||
if (enabledCategories !== undefined) updates.enabledCategories = enabledCategories
|
||||
if (categoryActions !== undefined) updates.categoryActions = categoryActions
|
||||
if (autoDetectCompanies !== undefined) updates.autoDetectCompanies = autoDetectCompanies
|
||||
|
||||
await userPreferences.upsert(userId, updates)
|
||||
respond.success(res, null, 'AI Control settings saved')
|
||||
}))
|
||||
|
||||
/**
|
||||
* GET /api/preferences/company-labels
|
||||
* Get company labels
|
||||
*/
|
||||
app.get('/api/preferences/company-labels', asyncHandler(async (req, res) => {
|
||||
const { userId } = req.query
|
||||
if (!userId) throw new ValidationError('userId is required')
|
||||
|
||||
const prefs = await userPreferences.getByUser(userId)
|
||||
const preferences = prefs?.preferences || userPreferences.getDefaults()
|
||||
|
||||
respond.success(res, preferences.companyLabels || [])
|
||||
}))
|
||||
|
||||
/**
|
||||
* POST /api/preferences/company-labels
|
||||
* Save/Update company label
|
||||
*/
|
||||
app.post('/api/preferences/company-labels', asyncHandler(async (req, res) => {
|
||||
const { userId, companyLabel } = req.body
|
||||
if (!userId) throw new ValidationError('userId is required')
|
||||
if (!companyLabel) throw new ValidationError('companyLabel is required')
|
||||
|
||||
const prefs = await userPreferences.getByUser(userId)
|
||||
const preferences = prefs?.preferences || userPreferences.getDefaults()
|
||||
|
||||
const companyLabels = preferences.companyLabels || []
|
||||
|
||||
// Generate ID if not provided
|
||||
if (!companyLabel.id) {
|
||||
companyLabel.id = `label_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
|
||||
// Update or add label
|
||||
const existingIndex = companyLabels.findIndex(l => l.id === companyLabel.id)
|
||||
if (existingIndex >= 0) {
|
||||
companyLabels[existingIndex] = companyLabel
|
||||
} else {
|
||||
companyLabels.push(companyLabel)
|
||||
}
|
||||
|
||||
await userPreferences.upsert(userId, { companyLabels })
|
||||
respond.success(res, companyLabel, 'Company label saved')
|
||||
}))
|
||||
|
||||
/**
|
||||
* DELETE /api/preferences/company-labels/:id
|
||||
* Delete company label
|
||||
*/
|
||||
app.delete('/api/preferences/company-labels/:id', asyncHandler(async (req, res) => {
|
||||
const { userId } = req.query
|
||||
const { id } = req.params
|
||||
if (!userId) throw new ValidationError('userId is required')
|
||||
if (!id) throw new ValidationError('label id is required')
|
||||
|
||||
const prefs = await userPreferences.getByUser(userId)
|
||||
const preferences = prefs?.preferences || userPreferences.getDefaults()
|
||||
|
||||
const companyLabels = (preferences.companyLabels || []).filter(l => l.id !== id)
|
||||
|
||||
await userPreferences.upsert(userId, { companyLabels })
|
||||
respond.success(res, null, 'Company label deleted')
|
||||
}))
|
||||
|
||||
// Legacy Stripe webhook endpoint
|
||||
app.use('/stripe', stripeRoutes)
|
||||
|
||||
|
||||
@@ -379,11 +379,17 @@ router.post('/sort',
|
||||
log.success(`${deletedLabels} old labels cleaned up`)
|
||||
}
|
||||
|
||||
// Create labels only for categories without native Gmail category
|
||||
// Create labels for categories and company labels
|
||||
const categories = sorter.getCategories()
|
||||
const labelMap = {}
|
||||
const companyLabelMap = {}
|
||||
|
||||
// Create labels for enabled categories only
|
||||
const enabledCategories = sorter.getEnabledCategories(preferences)
|
||||
for (const [key, cat] of Object.entries(categories)) {
|
||||
// Skip disabled categories
|
||||
if (!enabledCategories.includes(key)) continue
|
||||
|
||||
// Wenn Gmail-Kategorie existiert, diese verwenden
|
||||
const gmailCat = sorter.getGmailCategory(key)
|
||||
if (gmailCat) {
|
||||
@@ -401,6 +407,38 @@ router.post('/sort',
|
||||
}
|
||||
}
|
||||
|
||||
// Create company labels
|
||||
if (preferences.companyLabels?.length) {
|
||||
for (const companyLabel of preferences.companyLabels) {
|
||||
if (!companyLabel.enabled) continue
|
||||
|
||||
try {
|
||||
// Use orange color for company labels
|
||||
const label = await gmail.createLabel(companyLabel.name, '#ff9800')
|
||||
if (label) {
|
||||
companyLabelMap[companyLabel.id || companyLabel.name] = label.id
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn(`Failed to create company label: ${companyLabel.name}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create auto-detected company labels if enabled
|
||||
if (preferences.autoDetectCompanies) {
|
||||
const knownCompanies = ['Amazon', 'Google', 'Microsoft', 'Apple', 'Facebook', 'Twitter', 'LinkedIn', 'GitHub', 'Netflix', 'Spotify', 'PayPal', 'Stripe', 'Shopify', 'Uber', 'Airbnb', 'Dropbox', 'Slack', 'Zoom']
|
||||
for (const companyName of knownCompanies) {
|
||||
try {
|
||||
const label = await gmail.createLabel(companyName, '#ff9800')
|
||||
if (label) {
|
||||
companyLabelMap[companyName] = label.id
|
||||
}
|
||||
} catch (err) {
|
||||
// Label might already exist, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch and process ALL emails with pagination
|
||||
let pageToken = null
|
||||
let totalProcessed = 0
|
||||
@@ -420,27 +458,76 @@ router.post('/sort',
|
||||
// Get full email details
|
||||
const emails = await gmail.batchGetEmails(messages.map(m => m.id))
|
||||
|
||||
// Classify emails with AI
|
||||
const classified = await sorter.batchCategorize(
|
||||
emails.map(e => ({
|
||||
from: e.headers?.from || '',
|
||||
subject: e.headers?.subject || '',
|
||||
snippet: e.snippet || '',
|
||||
})),
|
||||
preferences
|
||||
)
|
||||
// Process each email: check company labels first, then AI categorization
|
||||
const processedEmails = []
|
||||
|
||||
for (const email of emails) {
|
||||
const emailData = {
|
||||
from: email.headers?.from || '',
|
||||
subject: email.headers?.subject || '',
|
||||
snippet: email.snippet || '',
|
||||
}
|
||||
|
||||
let category = null
|
||||
let companyLabel = null
|
||||
let skipAI = false
|
||||
|
||||
// PRIORITY 1: Check custom company labels
|
||||
if (preferences.companyLabels?.length) {
|
||||
for (const companyLabelConfig of preferences.companyLabels) {
|
||||
if (!companyLabelConfig.enabled) continue
|
||||
|
||||
if (sorter.matchesCompanyLabel(emailData, companyLabelConfig)) {
|
||||
category = companyLabelConfig.category || 'promotions'
|
||||
companyLabel = companyLabelConfig.name
|
||||
skipAI = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PRIORITY 2: Check auto-detected companies
|
||||
if (!skipAI && preferences.autoDetectCompanies) {
|
||||
const detected = sorter.detectCompany(emailData)
|
||||
if (detected) {
|
||||
category = 'promotions' // Default category for companies
|
||||
companyLabel = detected.label
|
||||
skipAI = true
|
||||
}
|
||||
}
|
||||
|
||||
// PRIORITY 3: AI categorization (if no company label matched)
|
||||
if (!skipAI) {
|
||||
const classification = await sorter.categorize(emailData, preferences)
|
||||
category = classification.category
|
||||
|
||||
// If category is disabled, fallback to review
|
||||
if (!enabledCategories.includes(category)) {
|
||||
category = 'review'
|
||||
}
|
||||
}
|
||||
|
||||
processedEmails.push({
|
||||
email,
|
||||
category,
|
||||
companyLabel,
|
||||
})
|
||||
}
|
||||
|
||||
// Apply labels/categories and actions
|
||||
for (let i = 0; i < classified.length; i++) {
|
||||
const email = emails[i]
|
||||
const { category } = classified[i].classification
|
||||
const action = sorter.getCategoryAction(category)
|
||||
for (const { email, category, companyLabel } of processedEmails) {
|
||||
const action = sorter.getCategoryAction(category, preferences)
|
||||
|
||||
try {
|
||||
const labelsToAdd = []
|
||||
const labelsToRemove = []
|
||||
|
||||
// Add label/category
|
||||
// Add company label if matched
|
||||
if (companyLabel && companyLabelMap[companyLabel]) {
|
||||
labelsToAdd.push(companyLabelMap[companyLabel])
|
||||
}
|
||||
|
||||
// Add category label/category
|
||||
if (labelMap[category]) {
|
||||
labelsToAdd.push(labelMap[category])
|
||||
}
|
||||
@@ -533,24 +620,75 @@ router.post('/sort',
|
||||
|
||||
if (!messages?.length) break
|
||||
|
||||
// Classify emails with AI
|
||||
const classified = await sorter.batchCategorize(
|
||||
messages.map(e => ({
|
||||
from: e.from?.emailAddress?.address || '',
|
||||
subject: e.subject || '',
|
||||
snippet: e.bodyPreview || '',
|
||||
})),
|
||||
preferences
|
||||
)
|
||||
// Process each email: check company labels first, then AI categorization
|
||||
const enabledCategories = sorter.getEnabledCategories(preferences)
|
||||
const processedEmails = []
|
||||
|
||||
for (const email of messages) {
|
||||
const emailData = {
|
||||
from: email.from?.emailAddress?.address || '',
|
||||
subject: email.subject || '',
|
||||
snippet: email.bodyPreview || '',
|
||||
}
|
||||
|
||||
let category = null
|
||||
let companyLabel = null
|
||||
let skipAI = false
|
||||
|
||||
// PRIORITY 1: Check custom company labels
|
||||
if (preferences.companyLabels?.length) {
|
||||
for (const companyLabelConfig of preferences.companyLabels) {
|
||||
if (!companyLabelConfig.enabled) continue
|
||||
|
||||
if (sorter.matchesCompanyLabel(emailData, companyLabelConfig)) {
|
||||
category = companyLabelConfig.category || 'promotions'
|
||||
companyLabel = companyLabelConfig.name
|
||||
skipAI = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PRIORITY 2: Check auto-detected companies
|
||||
if (!skipAI && preferences.autoDetectCompanies) {
|
||||
const detected = sorter.detectCompany(emailData)
|
||||
if (detected) {
|
||||
category = 'promotions' // Default category for companies
|
||||
companyLabel = detected.label
|
||||
skipAI = true
|
||||
}
|
||||
}
|
||||
|
||||
// PRIORITY 3: AI categorization (if no company label matched)
|
||||
if (!skipAI) {
|
||||
const classification = await sorter.categorize(emailData, preferences)
|
||||
category = classification.category
|
||||
|
||||
// If category is disabled, fallback to review
|
||||
if (!enabledCategories.includes(category)) {
|
||||
category = 'review'
|
||||
}
|
||||
}
|
||||
|
||||
processedEmails.push({
|
||||
email,
|
||||
category,
|
||||
companyLabel,
|
||||
})
|
||||
}
|
||||
|
||||
// Apply categories and actions
|
||||
for (let i = 0; i < classified.length; i++) {
|
||||
const email = messages[i]
|
||||
const { category } = classified[i].classification
|
||||
const action = sorter.getCategoryAction(category)
|
||||
for (const { email, category, companyLabel } of processedEmails) {
|
||||
const action = sorter.getCategoryAction(category, preferences)
|
||||
const catName = sorter.getLabelName(category)
|
||||
|
||||
try {
|
||||
// Add company label as category if matched
|
||||
if (companyLabel) {
|
||||
await outlook.addCategories(email.id, [companyLabel])
|
||||
}
|
||||
|
||||
// Add category
|
||||
await outlook.addCategories(email.id, [catName])
|
||||
|
||||
// Handle different actions
|
||||
|
||||
@@ -91,6 +91,40 @@ const CATEGORIES = {
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Known companies for automatic detection
|
||||
* Maps domain patterns to company names
|
||||
*/
|
||||
const KNOWN_COMPANIES = {
|
||||
'amazon.com': 'Amazon',
|
||||
'amazon.de': 'Amazon',
|
||||
'amazon.co.uk': 'Amazon',
|
||||
'amazon.fr': 'Amazon',
|
||||
'google.com': 'Google',
|
||||
'gmail.com': 'Google',
|
||||
'microsoft.com': 'Microsoft',
|
||||
'outlook.com': 'Microsoft',
|
||||
'hotmail.com': 'Microsoft',
|
||||
'apple.com': 'Apple',
|
||||
'icloud.com': 'Apple',
|
||||
'facebook.com': 'Facebook',
|
||||
'meta.com': 'Meta',
|
||||
'twitter.com': 'Twitter',
|
||||
'x.com': 'Twitter',
|
||||
'linkedin.com': 'LinkedIn',
|
||||
'github.com': 'GitHub',
|
||||
'netflix.com': 'Netflix',
|
||||
'spotify.com': 'Spotify',
|
||||
'paypal.com': 'PayPal',
|
||||
'stripe.com': 'Stripe',
|
||||
'shopify.com': 'Shopify',
|
||||
'uber.com': 'Uber',
|
||||
'airbnb.com': 'Airbnb',
|
||||
'dropbox.com': 'Dropbox',
|
||||
'slack.com': 'Slack',
|
||||
'zoom.us': 'Zoom',
|
||||
}
|
||||
|
||||
/**
|
||||
* AI Sorter Service Class
|
||||
*/
|
||||
@@ -137,9 +171,14 @@ export class AISorterService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get action for category
|
||||
* Get action for category (respects user preferences)
|
||||
*/
|
||||
getCategoryAction(key) {
|
||||
getCategoryAction(key, preferences = {}) {
|
||||
// Check for user override first
|
||||
if (preferences.categoryActions?.[key]) {
|
||||
return preferences.categoryActions[key]
|
||||
}
|
||||
// Return default action
|
||||
return CATEGORIES[key]?.action || 'inbox'
|
||||
}
|
||||
|
||||
@@ -150,6 +189,93 @@ export class AISorterService {
|
||||
return CATEGORIES[key]?.color || '#607d8b'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enabled categories based on user preferences
|
||||
*/
|
||||
getEnabledCategories(preferences = {}) {
|
||||
const enabled = preferences.enabledCategories || Object.keys(CATEGORIES)
|
||||
return enabled.filter(key => CATEGORIES[key]) // Only return valid categories
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect company from email address
|
||||
*/
|
||||
detectCompany(email) {
|
||||
if (!email?.from) return null
|
||||
|
||||
// Extract domain from email
|
||||
const emailMatch = email.from.match(/@([^\s>]+)/)
|
||||
if (!emailMatch) return null
|
||||
|
||||
const domain = emailMatch[1].toLowerCase()
|
||||
|
||||
// Check known companies
|
||||
if (KNOWN_COMPANIES[domain]) {
|
||||
return {
|
||||
name: KNOWN_COMPANIES[domain],
|
||||
domain,
|
||||
label: KNOWN_COMPANIES[domain],
|
||||
}
|
||||
}
|
||||
|
||||
// Check for subdomains (e.g., mail.amazon.com -> Amazon)
|
||||
const domainParts = domain.split('.')
|
||||
if (domainParts.length > 2) {
|
||||
const baseDomain = domainParts.slice(-2).join('.')
|
||||
if (KNOWN_COMPANIES[baseDomain]) {
|
||||
return {
|
||||
name: KNOWN_COMPANIES[baseDomain],
|
||||
domain: baseDomain,
|
||||
label: KNOWN_COMPANIES[baseDomain],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if email matches a company label condition
|
||||
*/
|
||||
matchesCompanyLabel(email, companyLabel) {
|
||||
if (!companyLabel?.enabled || !companyLabel?.condition) return false
|
||||
|
||||
const { condition } = companyLabel
|
||||
const from = email.from?.toLowerCase() || ''
|
||||
const subject = email.subject?.toLowerCase() || ''
|
||||
|
||||
// Simple condition parser: supports "from:domain.com" and "subject:keyword"
|
||||
if (condition.includes('from:')) {
|
||||
const domain = condition.split('from:')[1]?.trim().split(' ')[0]
|
||||
if (domain && from.includes(domain)) return true
|
||||
}
|
||||
|
||||
if (condition.includes('subject:')) {
|
||||
const keyword = condition.split('subject:')[1]?.trim().split(' ')[0]
|
||||
if (keyword && subject.includes(keyword)) return true
|
||||
}
|
||||
|
||||
// Support OR conditions
|
||||
if (condition.includes(' OR ')) {
|
||||
const parts = condition.split(' OR ')
|
||||
return parts.some(part => this.matchesCompanyLabel(email, { ...companyLabel, condition: part.trim() }))
|
||||
}
|
||||
|
||||
// Support AND conditions
|
||||
if (condition.includes(' AND ')) {
|
||||
const parts = condition.split(' AND ')
|
||||
return parts.every(part => this.matchesCompanyLabel(email, { ...companyLabel, condition: part.trim() }))
|
||||
}
|
||||
|
||||
// Simple domain match
|
||||
if (condition.includes('@')) {
|
||||
const domain = condition.split('@')[1]?.trim()
|
||||
if (domain && from.includes(domain)) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize a single email
|
||||
*/
|
||||
@@ -305,6 +431,14 @@ Respond ONLY with the JSON array.`
|
||||
_buildPreferenceContext(preferences) {
|
||||
const parts = []
|
||||
|
||||
// Get enabled categories
|
||||
const enabledCategories = this.getEnabledCategories(preferences)
|
||||
if (enabledCategories.length < Object.keys(CATEGORIES).length) {
|
||||
const disabled = Object.keys(CATEGORIES).filter(k => !enabledCategories.includes(k))
|
||||
parts.push(`DISABLED CATEGORIES (do not use): ${disabled.map(k => CATEGORIES[k].name).join(', ')}`)
|
||||
parts.push(`ONLY USE THESE CATEGORIES: ${enabledCategories.map(k => `${k} (${CATEGORIES[k].name})`).join(', ')}`)
|
||||
}
|
||||
|
||||
if (preferences.vipSenders?.length) {
|
||||
parts.push(`VIP Senders (always categorize as "vip"): ${preferences.vipSenders.join(', ')}`)
|
||||
}
|
||||
@@ -321,6 +455,14 @@ Respond ONLY with the JSON array.`
|
||||
parts.push(`Priority Topics (higher priority): ${preferences.priorityTopics.join(', ')}`)
|
||||
}
|
||||
|
||||
// Company labels context
|
||||
if (preferences.companyLabels?.length) {
|
||||
const activeLabels = preferences.companyLabels.filter(l => l.enabled)
|
||||
if (activeLabels.length > 0) {
|
||||
parts.push(`Company Labels (apply these labels when conditions match):\n${activeLabels.map(l => `- ${l.name}: ${l.condition} → ${l.category || 'promotions'}`).join('\n')}`)
|
||||
}
|
||||
}
|
||||
|
||||
return parts.length > 0 ? `USER PREFERENCES:\n${parts.join('\n')}\n` : ''
|
||||
}
|
||||
|
||||
|
||||
@@ -286,14 +286,44 @@ export const subscriptions = {
|
||||
* User preferences operations
|
||||
*/
|
||||
export const userPreferences = {
|
||||
/**
|
||||
* Get default preferences structure
|
||||
*/
|
||||
getDefaults() {
|
||||
return {
|
||||
vipSenders: [],
|
||||
enabledCategories: ['vip', 'customers', 'invoices', 'newsletters', 'promotions', 'social', 'security', 'calendar', 'review'],
|
||||
categoryActions: {},
|
||||
companyLabels: [],
|
||||
autoDetectCompanies: true,
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Merge preferences with defaults
|
||||
*/
|
||||
mergeWithDefaults(preferences) {
|
||||
const defaults = this.getDefaults()
|
||||
return {
|
||||
...defaults,
|
||||
...preferences,
|
||||
vipSenders: preferences.vipSenders || defaults.vipSenders,
|
||||
enabledCategories: preferences.enabledCategories || defaults.enabledCategories,
|
||||
categoryActions: preferences.categoryActions || defaults.categoryActions,
|
||||
companyLabels: preferences.companyLabels || defaults.companyLabels,
|
||||
autoDetectCompanies: preferences.autoDetectCompanies !== undefined ? preferences.autoDetectCompanies : defaults.autoDetectCompanies,
|
||||
}
|
||||
},
|
||||
|
||||
async getByUser(userId) {
|
||||
const pref = await db.findOne(Collections.USER_PREFERENCES, [
|
||||
Query.equal('userId', userId),
|
||||
])
|
||||
if (pref?.preferencesJson) {
|
||||
return { ...pref, preferences: JSON.parse(pref.preferencesJson) }
|
||||
const parsed = JSON.parse(pref.preferencesJson)
|
||||
return { ...pref, preferences: this.mergeWithDefaults(parsed) }
|
||||
}
|
||||
return pref
|
||||
return { ...pref, preferences: this.getDefaults() }
|
||||
},
|
||||
|
||||
async upsert(userId, preferences) {
|
||||
@@ -301,7 +331,14 @@ export const userPreferences = {
|
||||
Query.equal('userId', userId),
|
||||
])
|
||||
|
||||
const data = { preferencesJson: JSON.stringify(preferences) }
|
||||
// Merge with existing preferences if updating
|
||||
let mergedPreferences = preferences
|
||||
if (existing?.preferencesJson) {
|
||||
const existingPrefs = JSON.parse(existing.preferencesJson)
|
||||
mergedPreferences = { ...existingPrefs, ...preferences }
|
||||
}
|
||||
|
||||
const data = { preferencesJson: JSON.stringify(mergedPreferences) }
|
||||
|
||||
if (existing) {
|
||||
return db.update(Collections.USER_PREFERENCES, existing.$id, data)
|
||||
|
||||
Reference in New Issue
Block a user