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:
1 parent
1cec319fa8
commit
0b24596f15
3 files changed
+303
-21
No files matched your search
+88
-15
@@ -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 = {
|
||||
|
||||
Reference in new issue
Block a user