feat: Leads-Aktionsbuttons icon-only + WOID des Akquise-Tickets

- Maps/Website/Repo nur noch Icon (title/aria-label bleiben)
- WOID je Lead als klickbarer Chip -> /tickets?woid= (Lead->Kunde->Akquise-Ticket, live in useLeads gejoint)
- useWorkorders: woid server-seitig filterbar (macht den bisher wirkungslosen WOID-Filter nutzbar)
- TicketsPage: ?woid= Deep-Link liest Param, filtert ueber alle Status

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 4.8 committed 2026-07-16 18:40:40 +00:00
1 parent 4c305fcd46
commit fe3e39bd04
4 files changed
+93 -9

No files matched your search

+48 -1
View File
@@ -19,6 +19,51 @@ async function listAll(collection, queries = []) {
return out
}
/** Haengt an jeden Lead die WOID seines Akquise-Tickets:
* Lead -> Kunde (per E-Mail) -> Akquise-Workorder -> woid. Best effort;
* am Lead selbst ist keine WOID gespeichert, daher live gejoint. */
async function attachWoids(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) an die Leads haengen
return leads.map((l) => {
const hit = customerToWoid[emailToCustomer[norm(l)]]
return hit ? { ...l, woid: hit.woid, ticketId: hit.ticketId } : l
})
}
/** 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. */
@@ -31,7 +76,9 @@ export function useLeads() {
const fetchLeads = useCallback(async () => {
try {
const out = await listAll(COLLECTIONS.LEADS, [Query.orderDesc('leadScore')])
setLeads(out)
let enriched = out
try { enriched = await attachWoids(out) } catch { /* WOID-Join best effort */ }
setLeads(enriched)
setError(null)
} catch (err) {
if (err.code === 404 || err.message?.includes('not found')) {
+10
View File
@@ -11,6 +11,7 @@ function buildFiltersKey(filters = {}) {
status: filters.status ?? null,
type: filters.type ?? null,
priority: filters.priority ?? null,
woid: filters.woid ?? null,
})
}
@@ -87,6 +88,7 @@ export function useWorkorders(filters = {}) {
filters.status,
filters.type,
filters.priority,
filters.woid,
])
const fetchWorkorders = useCallback(async () => {
@@ -102,6 +104,9 @@ export function useWorkorders(filters = {}) {
if (activeFilters.priority?.length > 0) {
filtered = filtered.filter(wo => activeFilters.priority.includes(wo.priority))
}
if (activeFilters.woid) {
filtered = filtered.filter(wo => String(wo.woid) === String(activeFilters.woid))
}
if (activeFilters.limit) {
filtered = filtered.slice(0, activeFilters.limit)
}
@@ -140,6 +145,11 @@ export function useWorkorders(filters = {}) {
queries.push(Query.equal('assignedTo', activeFilters.assignedTo))
}
// WOID-Direktsuche (z. B. Deep-Link aus den Leads: /tickets?woid=123)
if (activeFilters.woid) {
queries.push(Query.equal('woid', [String(activeFilters.woid)]))
}
// Debug: Zeige Collection ID
if (import.meta.env.DEV) {
console.log('📋 Fetching workorders:')