Files
Emailsorter/server/services/database.mjs
ANDJ 18c11d27bc 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
2026-01-26 17:49:39 +01:00

466 lines
12 KiB
JavaScript

/**
* Database Service
* Centralized Appwrite database operations
*/
import { Client, Databases, Query, ID } from 'node-appwrite'
import { config } from '../config/index.mjs'
import { NotFoundError } from '../middleware/errorHandler.mjs'
// Initialize Appwrite client
const client = new Client()
.setEndpoint(config.appwrite.endpoint)
.setProject(config.appwrite.projectId)
.setKey(config.appwrite.apiKey)
const databases = new Databases(client)
const DB_ID = config.appwrite.databaseId
/**
* Collection names
*/
export const Collections = {
PRODUCTS: 'products',
QUESTIONS: 'questions',
SUBMISSIONS: 'submissions',
ANSWERS: 'answers',
ORDERS: 'orders',
EMAIL_ACCOUNTS: 'email_accounts',
EMAIL_STATS: 'email_stats',
EMAIL_DIGESTS: 'email_digests',
SUBSCRIPTIONS: 'subscriptions',
USER_PREFERENCES: 'user_preferences',
}
/**
* Generic database operations
*/
export const db = {
/**
* Create a document
*/
async create(collection, data, id = ID.unique()) {
return await databases.createDocument(DB_ID, collection, id, data)
},
/**
* Get a document by ID
*/
async get(collection, id) {
try {
return await databases.getDocument(DB_ID, collection, id)
} catch (error) {
if (error.code === 404) {
throw new NotFoundError(collection)
}
throw error
}
},
/**
* Update a document
*/
async update(collection, id, data) {
return await databases.updateDocument(DB_ID, collection, id, data)
},
/**
* Delete a document
*/
async delete(collection, id) {
return await databases.deleteDocument(DB_ID, collection, id)
},
/**
* List documents with optional queries
*/
async list(collection, queries = []) {
const response = await databases.listDocuments(DB_ID, collection, queries)
return response.documents
},
/**
* Find one document matching queries
*/
async findOne(collection, queries) {
const docs = await this.list(collection, [...queries, Query.limit(1)])
return docs[0] || null
},
/**
* Check if document exists
*/
async exists(collection, id) {
try {
await databases.getDocument(DB_ID, collection, id)
return true
} catch {
return false
}
},
/**
* Count documents
*/
async count(collection, queries = []) {
const response = await databases.listDocuments(DB_ID, collection, [
...queries,
Query.limit(1),
])
return response.total
},
}
/**
* Product operations
*/
export const products = {
async getBySlug(slug) {
return db.findOne(Collections.PRODUCTS, [
Query.equal('slug', slug),
Query.equal('isActive', true),
])
},
async getActive() {
return db.list(Collections.PRODUCTS, [Query.equal('isActive', true)])
},
}
/**
* Questions operations
*/
export const questions = {
async getByProduct(productId) {
return db.list(Collections.QUESTIONS, [
Query.equal('productId', productId),
Query.equal('isActive', true),
Query.orderAsc('step'),
Query.orderAsc('order'),
])
},
}
/**
* Submissions operations
*/
export const submissions = {
async create(data) {
return db.create(Collections.SUBMISSIONS, data)
},
async updateStatus(id, status) {
return db.update(Collections.SUBMISSIONS, id, { status })
},
async getByUser(userId) {
return db.list(Collections.SUBMISSIONS, [Query.equal('userId', userId)])
},
}
/**
* Email accounts operations
*/
export const emailAccounts = {
async create(data) {
return db.create(Collections.EMAIL_ACCOUNTS, data)
},
async getByUser(userId) {
return db.list(Collections.EMAIL_ACCOUNTS, [
Query.equal('userId', userId),
Query.equal('isActive', true),
])
},
async get(id) {
return db.get(Collections.EMAIL_ACCOUNTS, id)
},
async updateLastSync(id) {
return db.update(Collections.EMAIL_ACCOUNTS, id, {
lastSync: new Date().toISOString(),
})
},
async deactivate(id) {
return db.update(Collections.EMAIL_ACCOUNTS, id, { isActive: false })
},
}
/**
* Email stats operations
*/
export const emailStats = {
async getByUser(userId) {
return db.findOne(Collections.EMAIL_STATS, [Query.equal('userId', userId)])
},
async create(userId, data) {
return db.create(Collections.EMAIL_STATS, { userId, ...data })
},
async increment(userId, counts) {
const stats = await this.getByUser(userId)
if (stats) {
return db.update(Collections.EMAIL_STATS, stats.$id, {
totalSorted: (stats.totalSorted || 0) + (counts.total || 0),
todaySorted: (stats.todaySorted || 0) + (counts.today || 0),
weekSorted: (stats.weekSorted || 0) + (counts.week || 0),
timeSavedMinutes: (stats.timeSavedMinutes || 0) + (counts.timeSaved || 0),
})
} else {
return this.create(userId, {
totalSorted: counts.total || 0,
todaySorted: counts.today || 0,
weekSorted: counts.week || 0,
timeSavedMinutes: counts.timeSaved || 0,
categoriesJson: '{}',
})
}
},
async updateCategories(userId, categories) {
const stats = await this.getByUser(userId)
if (stats) {
return db.update(Collections.EMAIL_STATS, stats.$id, {
categoriesJson: JSON.stringify(categories),
})
}
return null
},
async resetDaily() {
// Reset daily counters - would be called by a cron job
const allStats = await db.list(Collections.EMAIL_STATS, [])
for (const stat of allStats) {
await db.update(Collections.EMAIL_STATS, stat.$id, { todaySorted: 0 })
}
},
async resetWeekly() {
// Reset weekly counters - would be called by a cron job
const allStats = await db.list(Collections.EMAIL_STATS, [])
for (const stat of allStats) {
await db.update(Collections.EMAIL_STATS, stat.$id, {
weekSorted: 0,
categoriesJson: '{}',
})
}
},
}
/**
* Subscriptions operations
*/
export const subscriptions = {
async getByUser(userId) {
return db.findOne(Collections.SUBSCRIPTIONS, [Query.equal('userId', userId)])
},
async getByStripeId(stripeSubscriptionId) {
return db.findOne(Collections.SUBSCRIPTIONS, [
Query.equal('stripeSubscriptionId', stripeSubscriptionId),
])
},
async create(data) {
return db.create(Collections.SUBSCRIPTIONS, data)
},
async update(id, data) {
return db.update(Collections.SUBSCRIPTIONS, id, data)
},
async upsertByUser(userId, data) {
const existing = await this.getByUser(userId)
if (existing) {
return this.update(existing.$id, data)
}
return this.create({ userId, ...data })
},
}
/**
* 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) {
const parsed = JSON.parse(pref.preferencesJson)
return { ...pref, preferences: this.mergeWithDefaults(parsed) }
}
return { ...pref, preferences: this.getDefaults() }
},
async upsert(userId, preferences) {
const existing = await db.findOne(Collections.USER_PREFERENCES, [
Query.equal('userId', userId),
])
// 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)
}
return db.create(Collections.USER_PREFERENCES, { userId, ...data })
},
}
/**
* Orders operations
*/
export const orders = {
async create(submissionId, orderData) {
return db.create(Collections.ORDERS, {
submissionId,
orderDataJson: JSON.stringify(orderData),
})
},
}
/**
* Email digests operations
*/
export const emailDigests = {
async create(userId, digestData) {
const date = new Date().toISOString().split('T')[0] // YYYY-MM-DD
return db.create(Collections.EMAIL_DIGESTS, {
userId,
date,
statsJson: JSON.stringify(digestData.stats || {}),
highlightsJson: JSON.stringify(digestData.highlights || []),
suggestionsJson: JSON.stringify(digestData.suggestions || []),
totalSorted: digestData.totalSorted || 0,
inboxCleared: digestData.inboxCleared || 0,
timeSavedMinutes: digestData.timeSavedMinutes || 0,
createdAt: new Date().toISOString(),
})
},
async getByUserToday(userId) {
const today = new Date().toISOString().split('T')[0]
const digest = await db.findOne(Collections.EMAIL_DIGESTS, [
Query.equal('userId', userId),
Query.equal('date', today),
])
if (digest) {
return {
...digest,
stats: JSON.parse(digest.statsJson || '{}'),
highlights: JSON.parse(digest.highlightsJson || '[]'),
suggestions: JSON.parse(digest.suggestionsJson || '[]'),
}
}
return null
},
async getByUserRecent(userId, days = 7) {
const startDate = new Date()
startDate.setDate(startDate.getDate() - days)
const digests = await db.list(Collections.EMAIL_DIGESTS, [
Query.equal('userId', userId),
Query.greaterThanEqual('date', startDate.toISOString().split('T')[0]),
Query.orderDesc('date'),
])
return digests.map(d => ({
...d,
stats: JSON.parse(d.statsJson || '{}'),
highlights: JSON.parse(d.highlightsJson || '[]'),
suggestions: JSON.parse(d.suggestionsJson || '[]'),
}))
},
async updateToday(userId, updates) {
const today = new Date().toISOString().split('T')[0]
const existing = await db.findOne(Collections.EMAIL_DIGESTS, [
Query.equal('userId', userId),
Query.equal('date', today),
])
if (existing) {
const updateData = {}
if (updates.stats) updateData.statsJson = JSON.stringify(updates.stats)
if (updates.highlights) updateData.highlightsJson = JSON.stringify(updates.highlights)
if (updates.suggestions) updateData.suggestionsJson = JSON.stringify(updates.suggestions)
if (updates.totalSorted !== undefined) {
updateData.totalSorted = (existing.totalSorted || 0) + updates.totalSorted
}
if (updates.inboxCleared !== undefined) {
updateData.inboxCleared = (existing.inboxCleared || 0) + updates.inboxCleared
}
if (updates.timeSavedMinutes !== undefined) {
updateData.timeSavedMinutes = (existing.timeSavedMinutes || 0) + updates.timeSavedMinutes
}
return db.update(Collections.EMAIL_DIGESTS, existing.$id, updateData)
}
// Create new digest for today
return this.create(userId, {
stats: updates.stats || {},
highlights: updates.highlights || [],
suggestions: updates.suggestions || [],
totalSorted: updates.totalSorted || 0,
inboxCleared: updates.inboxCleared || 0,
timeSavedMinutes: updates.timeSavedMinutes || 0,
})
},
}
export default {
db,
products,
questions,
submissions,
emailAccounts,
emailStats,
emailDigests,
subscriptions,
userPreferences,
orders,
Collections,
}