LeadsPage und useLeads entfernt (seit Lead-Pause unbenutzt)
Ausgangswerte: 312 Tests, JS 680818 B, CSS 64433 B Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e28162eabf
commit
18e8f3bda5
2 files changed
-896
No files matched your search
@@ -1,282 +0,0 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import { COLLECTIONS, DATABASE_ID, Query, databases } from '../lib/appwrite'
|
||||
|
||||
const PAGE = 100
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/** Haengt an jeden Lead Verweise, die nicht am Lead selbst gespeichert sind,
|
||||
* sondern ueber den Kunden verknuepft werden — via Lead -> Kunde (per E-Mail):
|
||||
* - woid: WOID des Akquise-Tickets (workorders type=Akquise)
|
||||
* - previewUrl: URL der erstellten, deployten Website (websiteProjects)
|
||||
* Best effort; Fehler brechen die Lead-Liste nicht. */
|
||||
async function attachLeadLinks(leads) {
|
||||
const norm = (l) => (l.email || l.portalLogin || '').trim().toLowerCase()
|
||||
const emails = [...new Set(leads.map(norm).filter(Boolean))]
|
||||
if (!emails.length) return leads
|
||||
|
||||
// 1) Kunden per E-Mail -> customerId
|
||||
const emailToCustomer = {}
|
||||
const customerIds = []
|
||||
for (let i = 0; i < emails.length; i += 90) {
|
||||
const docs = await listAll(COLLECTIONS.CUSTOMERS, [Query.equal('email', emails.slice(i, i + 90))])
|
||||
for (const c of docs) {
|
||||
const e = (c.email || '').trim().toLowerCase()
|
||||
if (e && !emailToCustomer[e]) {
|
||||
emailToCustomer[e] = c.$id
|
||||
customerIds.push(c.$id)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!customerIds.length) return leads
|
||||
|
||||
// 2) Akquise-Tickets per customerId -> kleinste (urspruengliche) WOID je Kunde
|
||||
const customerToWoid = {}
|
||||
for (let i = 0; i < customerIds.length; i += 90) {
|
||||
const docs = await listAll(COLLECTIONS.WORKORDERS, [
|
||||
Query.equal('customerId', customerIds.slice(i, i + 90)),
|
||||
Query.equal('type', ['Akquise']),
|
||||
])
|
||||
for (const w of docs) {
|
||||
const n = parseInt(w.woid, 10)
|
||||
if (!w.customerId || isNaN(n)) continue
|
||||
const cur = customerToWoid[w.customerId]
|
||||
if (!cur || n < cur.n) customerToWoid[w.customerId] = { n, woid: w.woid, ticketId: w.$id }
|
||||
}
|
||||
}
|
||||
|
||||
// 3) deployte Preview je Kunde -> URL der erstellten Website.
|
||||
// Die websiteProjects-Collection ist klein -> komplett laden + lokal zuordnen
|
||||
// (die Preview haengt am selben Kunden, aber an einem anderen Repo als lead.repoUrl).
|
||||
const customerToPreview = {}
|
||||
const idSet = new Set(customerIds)
|
||||
for (const p of await listAll(COLLECTIONS.WEBSITE_PROJECTS)) {
|
||||
if (!idSet.has(p.customerId) || customerToPreview[p.customerId]) continue
|
||||
const ready = p.status === 'deployed' || p.status === 'ready' || p.provisioningStatus === 'ready'
|
||||
const url = p.previewUrl || (p.subdomain ? `https://${p.subdomain}.project.webklar.com` : '')
|
||||
if (ready && url) customerToPreview[p.customerId] = url
|
||||
}
|
||||
|
||||
// 4) an die Leads haengen
|
||||
return leads.map((l) => {
|
||||
const cid = emailToCustomer[norm(l)]
|
||||
const t = customerToWoid[cid]
|
||||
const previewUrl = customerToPreview[cid]
|
||||
if (!t && !previewUrl) return l
|
||||
return {
|
||||
...l,
|
||||
...(t ? { woid: t.woid, ticketId: t.ticketId } : {}),
|
||||
...(previewUrl ? { previewUrl } : {}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 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 = await listAll(COLLECTIONS.LEADS, [Query.orderDesc('leadScore')])
|
||||
let enriched = out
|
||||
try { enriched = await attachLeadLinks(out) } catch { /* Live-Join best effort */ }
|
||||
setLeads(enriched)
|
||||
setError(null)
|
||||
} catch (err) {
|
||||
if (err.code === 404 || err.message?.includes('not found')) {
|
||||
setLeads([])
|
||||
setError(null)
|
||||
} else {
|
||||
setError(err.message)
|
||||
setLeads([])
|
||||
}
|
||||
} 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, {
|
||||
...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, 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 = {
|
||||
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 }
|
||||
}
|
||||
Reference in new issue
Block a user