feat: CRM-Tab Leads - Lead-Visualisierung aus Appwrite + Sucheinstellungen der 06:00-Routine

- Neue Seite /leads unter CRM: Lead-Karten (Score, Google-Bewertung,
  Website-Status, Kontakt, Maps/Website/Repo-Links), KPI-Zeile,
  Status-Workflow (neu/kontaktiert/interessiert/gewonnen/abgelehnt),
  Notizen, Portal-Zugangsdaten
- Sucheinstellungen (Branche, Staedte oder ganz Deutschland, max. Leads
  pro Stadt, Mindestbewertung, E-Mail-Pflicht, Website-Kriterium,
  Zusatzhinweise) -> Appwrite leadSettings, gelesen von der Routine
  ueber webklar-hook /config
- Ersetzt die Excel-Masterliste als Lead-Ansicht

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Fable 5 committed 2026-07-14 18:09:23 +00:00
1 parent 5786eed3ba
commit 1cec319fa8
5 files changed
+591 -1

No files matched your search

+141
View File
@@ -0,0 +1,141 @@
import { useState, useEffect, useCallback } from 'react'
import { COLLECTIONS, DATABASE_ID, Query, databases } from '../lib/appwrite'
const PAGE = 100
/** Recherche-Leads aus der taeglichen 06:00-Routine (Appwrite `leads`). */
export function useLeads() {
const [leads, setLeads] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const fetchLeads = useCallback(async () => {
try {
const out = []
let offset = 0
for (;;) {
const res = await databases.listDocuments(DATABASE_ID, COLLECTIONS.LEADS, [
Query.orderDesc('leadScore'),
Query.limit(PAGE),
Query.offset(offset),
])
out.push(...(res.documents || []))
if ((res.documents || []).length < PAGE) break
offset += PAGE
}
setLeads(out)
setError(null)
} catch (err) {
if (err.code === 404 || err.message?.includes('not found')) {
setLeads([])
setError(null)
} else {
setError(err.message)
setLeads([])
}
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchLeads()
}, [fetchLeads])
const updateLead = async (id, data) => {
try {
const doc = await databases.updateDocument(DATABASE_ID, COLLECTIONS.LEADS, id, {
...data,
updatedAt: new Date().toISOString(),
})
setLeads((prev) => prev.map((l) => (l.$id === id ? doc : l)))
return { success: true, data: doc }
} catch (err) {
return { success: false, error: err.message }
}
}
const deleteLead = async (id) => {
try {
await databases.deleteDocument(DATABASE_ID, COLLECTIONS.LEADS, id)
setLeads((prev) => prev.filter((l) => l.$id !== id))
return { success: true }
} catch (err) {
return { success: false, error: err.message }
}
}
return { leads, loading, error, refresh: fetchLeads, updateLead, deleteLead }
}
export const DEFAULT_LEAD_SETTINGS = {
branche: 'Autolackiererei',
staedte: [],
ganzDeutschland: false,
maxLeadsProStadt: 5,
minBewertung: 4.0,
emailPflicht: true,
nurSchlechteWebsite: true,
zusatzHinweise: '',
}
/** Sucheinstellungen der Routine (Appwrite `leadSettings`, Dokument `main`).
* Die Cloud-Routine liest sie vor jedem Lauf ueber den Server-Hook (/config). */
export function useLeadSettings() {
const [settings, setSettings] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const fetchSettings = useCallback(async () => {
try {
const doc = await databases.getDocument(DATABASE_ID, COLLECTIONS.LEAD_SETTINGS, 'main')
setSettings(doc)
setError(null)
} catch (err) {
if (err.code === 404 || err.message?.includes('not found')) {
setSettings({ ...DEFAULT_LEAD_SETTINGS })
setError(null)
} else {
setError(err.message)
}
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchSettings()
}, [fetchSettings])
const saveSettings = async (data) => {
const payload = {
branche: data.branche || '',
staedte: data.staedte || [],
ganzDeutschland: Boolean(data.ganzDeutschland),
maxLeadsProStadt: Number(data.maxLeadsProStadt) || 5,
minBewertung: Number(data.minBewertung) || 0,
emailPflicht: Boolean(data.emailPflicht),
nurSchlechteWebsite: Boolean(data.nurSchlechteWebsite),
zusatzHinweise: data.zusatzHinweise || '',
updatedAt: new Date().toISOString(),
}
try {
let doc
try {
doc = await databases.updateDocument(DATABASE_ID, COLLECTIONS.LEAD_SETTINGS, 'main', payload)
} catch (err) {
if (err.code === 404) {
doc = await databases.createDocument(DATABASE_ID, COLLECTIONS.LEAD_SETTINGS, 'main', payload)
} else {
throw err
}
}
setSettings(doc)
return { success: true, data: doc }
} catch (err) {
return { success: false, error: err.message }
}
}
return { settings, loading, error, refresh: fetchSettings, saveSettings }
}