feat: Lead-Pipeline sichtbar - 5-Min-Zyklusbalken + Refresh-Trigger, Schritt-Fortschritt pro Lead, Pipeline-Log & KI-E-Mail-Entwurf

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Fable 5 committed 2026-07-14 20:57:18 +00:00
1 parent 1cec319fa8
commit 0b24596f15
3 files changed
+303 -21

No files matched your search

+88 -15
View File
@@ -1,28 +1,36 @@
import { useState, useEffect, useCallback } from 'react'
import { useState, useEffect, useCallback, useMemo } from 'react'
import { COLLECTIONS, DATABASE_ID, Query, databases } from '../lib/appwrite'
const PAGE = 100
/** Recherche-Leads aus der taeglichen 06:00-Routine (Appwrite `leads`). */
async function listAll(collection, queries = []) {
const out = []
let offset = 0
for (;;) {
const res = await databases.listDocuments(DATABASE_ID, collection, [
...queries,
Query.limit(PAGE),
Query.offset(offset),
])
out.push(...(res.documents || []))
if ((res.documents || []).length < PAGE) break
offset += PAGE
}
return out
}
/** Recherche-Leads aus der taeglichen 06:00-Routine (Appwrite `leads`).
* Pollt automatisch nach — schneller, solange die 5-Minuten-Pipeline
* (processor.py auf dem Server) gerade Leads verarbeitet. */
export function useLeads() {
const [leads, setLeads] = useState([])
const [pipelines, setPipelines] = 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
}
const out = await listAll(COLLECTIONS.LEADS, [Query.orderDesc('leadScore')])
setLeads(out)
setError(null)
} catch (err) {
@@ -36,12 +44,31 @@ export function useLeads() {
} finally {
setLoading(false)
}
// Schritt-Log + E-Mail-Entwurf pro Lead (Zusatz-Collection, best effort)
try {
const docs = await listAll(COLLECTIONS.LEAD_PIPELINE)
const map = {}
for (const d of docs) if (d.leadId) map[d.leadId] = d
setPipelines(map)
} catch {
setPipelines({})
}
}, [])
useEffect(() => {
fetchLeads()
}, [fetchLeads])
// Live-Polling: 5s waehrend die Pipeline arbeitet, sonst alle 20s
const pipelineActive = useMemo(
() => leads.some((l) => l.pipelineStatus === 'running' || l.pipelineStatus === 'pending'),
[leads]
)
useEffect(() => {
const t = setInterval(fetchLeads, pipelineActive ? 5000 : 20000)
return () => clearInterval(t)
}, [fetchLeads, pipelineActive])
const updateLead = async (id, data) => {
try {
const doc = await databases.updateDocument(DATABASE_ID, COLLECTIONS.LEADS, id, {
@@ -65,7 +92,53 @@ export function useLeads() {
}
}
return { leads, loading, error, refresh: fetchLeads, updateLead, deleteLead }
return { leads, pipelines, loading, error, refresh: fetchLeads, updateLead, deleteLead }
}
/** 5-Minuten-Zyklus des Server-Processors (leadSettings: lastRunAt/nextRunAt/
* workerBusy) + manueller Sofort-Trigger (runNow=true, der Processor pollt
* das Flag alle 5 Sekunden). */
export function useLeadCycle() {
const [cycle, setCycle] = useState(null)
const [triggering, setTriggering] = useState(false)
const fetchCycle = useCallback(async () => {
try {
const doc = await databases.getDocument(DATABASE_ID, COLLECTIONS.LEAD_SETTINGS, 'main')
setCycle({
lastRunAt: doc.lastRunAt || null,
nextRunAt: doc.nextRunAt || null,
runNow: Boolean(doc.runNow),
workerBusy: Boolean(doc.workerBusy),
})
} catch {
setCycle(null)
}
}, [])
useEffect(() => {
fetchCycle()
const t = setInterval(fetchCycle, 10000)
return () => clearInterval(t)
}, [fetchCycle])
const triggerNow = async () => {
setTriggering(true)
try {
await databases.updateDocument(DATABASE_ID, COLLECTIONS.LEAD_SETTINGS, 'main', {
runNow: true,
updatedAt: new Date().toISOString(),
})
await fetchCycle()
return { success: true }
} catch (err) {
return { success: false, error: err.message }
} finally {
setTriggering(false)
}
}
return { cycle, triggering, triggerNow, refresh: fetchCycle }
}
export const DEFAULT_LEAD_SETTINGS = {