Tickets: Hooks useWorkorder, Zaehler und Zeitsummen; useWorkorders verwirft veraltete Antworten
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0b53fc825a
commit
ab42918bbc
4 files changed
+206
-2
No files matched your search
@@ -0,0 +1,51 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { databases, DATABASE_ID, COLLECTIONS, Query } from '../lib/appwrite'
|
||||||
|
import { LIST_TABS, LIST_DEFAULTS, countFilters } from '../lib/ticketListUrl'
|
||||||
|
|
||||||
|
// Zahl je Reiter der Ticketliste: counts[tabId] ist eine Zahl, null bei Fehler,
|
||||||
|
// vor der ersten Antwort fehlt der Schluessel. Die Zahlen haengen nur an Art und "Dringend",
|
||||||
|
// nie am Reiter oder der Suche. Alte Zahlen bleiben stehen, bis die neue Antwort da ist.
|
||||||
|
function countQueries(filters) {
|
||||||
|
// Dieselben Bedingungen wie in useWorkorders, nur mit limit(1): gebraucht wird allein "total"
|
||||||
|
const queries = [Query.limit(1)]
|
||||||
|
if (filters.status && filters.status.length > 0) queries.push(Query.equal('status', filters.status))
|
||||||
|
if (filters.type && filters.type.length > 0) queries.push(Query.equal('type', filters.type))
|
||||||
|
if (filters.excludeType) queries.push(Query.notEqual('type', filters.excludeType))
|
||||||
|
if (filters.priority && filters.priority.length > 0) queries.push(Query.equal('priority', filters.priority))
|
||||||
|
return queries
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTicketCounts({ typeId, urgentOnly } = {}) {
|
||||||
|
const [counts, setCounts] = useState({})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let stale = false
|
||||||
|
const state = { ...LIST_DEFAULTS, typeId, urgentOnly }
|
||||||
|
|
||||||
|
Promise.allSettled(
|
||||||
|
LIST_TABS.map((tab) => databases.listDocuments(
|
||||||
|
DATABASE_ID,
|
||||||
|
COLLECTIONS.WORKORDERS,
|
||||||
|
countQueries(countFilters(state, tab.id))
|
||||||
|
))
|
||||||
|
).then((results) => {
|
||||||
|
if (stale) return
|
||||||
|
|
||||||
|
const next = {}
|
||||||
|
results.forEach((result, index) => {
|
||||||
|
const tabId = LIST_TABS[index].id
|
||||||
|
if (result.status === 'fulfilled' && typeof result.value?.total === 'number') {
|
||||||
|
next[tabId] = result.value.total
|
||||||
|
} else {
|
||||||
|
next[tabId] = null
|
||||||
|
console.warn('Ticket-Zaehler nicht geladen:', tabId, result.reason)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setCounts(next)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => { stale = true }
|
||||||
|
}, [typeId, urgentOnly])
|
||||||
|
|
||||||
|
return { counts }
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { useState, useEffect, useRef } from 'react'
|
||||||
|
import { databases, DATABASE_ID, COLLECTIONS, Query } from '../lib/appwrite'
|
||||||
|
import { planTimeLoad, blockMinutes, mapLimit, TIME_PARALLEL, TIME_QUERY_LIMIT } from '../lib/ticketTimes'
|
||||||
|
|
||||||
|
// Spalte "Zeit" der Ticketliste: Arbeitszeit je woid, in Bloecken nachgeladen (nur Lesen).
|
||||||
|
// byWoid ist eine Map woid -> Minuten (null = nicht zu ermitteln). Sie wird an Ort und Stelle
|
||||||
|
// ergaenzt, `version` zaehlt hoch, wenn sich etwas geaendert hat (fuer useMemo-Abhaengigkeiten).
|
||||||
|
// Der Zwischenspeicher gilt nur fuer die Lebensdauer der Seite: wer von einer Ticketseite
|
||||||
|
// zurueckkehrt, sieht frische Zahlen.
|
||||||
|
export function useTicketMinutes(tickets) {
|
||||||
|
const cache = useRef(new Map())
|
||||||
|
const inFlight = useRef(new Set())
|
||||||
|
const mounted = useRef(true)
|
||||||
|
const [version, setVersion] = useState(0)
|
||||||
|
const [capped, setCapped] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
mounted.current = true
|
||||||
|
return () => { mounted.current = false }
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const byWoid = cache.current
|
||||||
|
const running = inFlight.current
|
||||||
|
// Ein woid, dessen Abfrage noch laeuft, gilt als bekannt (kein Block doppelt anfragen)
|
||||||
|
const known = { has: (woid) => byWoid.has(woid) || running.has(woid) }
|
||||||
|
const plan = planTimeLoad(tickets, known)
|
||||||
|
setCapped(plan.capped)
|
||||||
|
if (plan.blocks.length === 0) return
|
||||||
|
|
||||||
|
for (const block of plan.blocks) {
|
||||||
|
for (const woid of block) running.add(woid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auch wenn sich `tickets` inzwischen geaendert hat, wird das Ergebnis eingetragen:
|
||||||
|
// sonst bliebe das woid ungeladen und ohne Abfrage.
|
||||||
|
mapLimit(plan.blocks, TIME_PARALLEL, async (block) => {
|
||||||
|
const response = await databases.listDocuments(DATABASE_ID, COLLECTIONS.WORKSHEETS, [
|
||||||
|
Query.equal('woid', block),
|
||||||
|
Query.select(['woid', 'totalTime', 'supporters', 'isComment']),
|
||||||
|
Query.limit(TIME_QUERY_LIMIT),
|
||||||
|
])
|
||||||
|
return blockMinutes(block, response.documents, response.total)
|
||||||
|
}).then((results) => {
|
||||||
|
results.forEach((result, index) => {
|
||||||
|
const sums = result.ok ? result.value : null
|
||||||
|
if (!sums) console.warn('Zeitsummen nicht geladen:', result.ok ? 'Antwort abgeschnitten' : result.error)
|
||||||
|
for (const woid of plan.blocks[index]) {
|
||||||
|
byWoid.set(woid, sums ? sums[woid] ?? null : null)
|
||||||
|
running.delete(woid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (mounted.current) setVersion((v) => v + 1)
|
||||||
|
})
|
||||||
|
}, [tickets])
|
||||||
|
|
||||||
|
return { byWoid: cache.current, capped, version }
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
|
import { databases, DATABASE_ID, COLLECTIONS, Query } from '../lib/appwrite'
|
||||||
|
import { lookupWorkorder } from '../lib/workorderLookup'
|
||||||
|
import { createRequestGuard } from '../lib/requestGuard'
|
||||||
|
|
||||||
|
// Ein Ticket zu einer Adresse wie /tickets/<ref> (Nummer oder Dokument-ID).
|
||||||
|
// Der Zustand merkt sich zu welcher Adresse er gehoert: steht schon eine neue Adresse in der
|
||||||
|
// URL, zeigt der Hook "laedt" und nie das vorige Ticket.
|
||||||
|
const LOADING = { ref: undefined, status: 'loading', ticket: null, error: null }
|
||||||
|
|
||||||
|
// Neueste Anfrage zur Nummer (woid), null wenn es keine gibt
|
||||||
|
async function findByWoid(woid) {
|
||||||
|
const response = await databases.listDocuments(DATABASE_ID, COLLECTIONS.WORKORDERS, [
|
||||||
|
Query.equal('woid', [String(woid)]),
|
||||||
|
Query.orderDesc('$createdAt'),
|
||||||
|
Query.limit(1),
|
||||||
|
])
|
||||||
|
return response.documents[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getById(id) {
|
||||||
|
return databases.getDocument(DATABASE_ID, COLLECTIONS.WORKORDERS, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWorkorder(ref) {
|
||||||
|
const [state, setState] = useState(LOADING)
|
||||||
|
// Nur die juengste Anfrage darf das Ergebnis uebernehmen
|
||||||
|
const guard = useRef(createRequestGuard())
|
||||||
|
const current = state.ref === ref ? state : LOADING
|
||||||
|
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
const requestId = guard.current.next()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await lookupWorkorder(String(ref ?? ''), { findByWoid, getById })
|
||||||
|
if (!guard.current.isCurrent(requestId)) return
|
||||||
|
|
||||||
|
setState(
|
||||||
|
result.status === 'ok'
|
||||||
|
? { ref, status: 'ok', ticket: result.ticket, error: null }
|
||||||
|
: { ref, status: 'notfound', ticket: null, error: null }
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
if (!guard.current.isCurrent(requestId)) return
|
||||||
|
|
||||||
|
let errorMessage = err.message || 'Fehler beim Laden des Tickets'
|
||||||
|
if (err.code === 401 || errorMessage.includes('not authorized') || errorMessage.includes('Unauthorized')) {
|
||||||
|
errorMessage = 'Berechtigung fehlt: Bitte überprüfe die Read-Berechtigungen der Collection in Appwrite. Die Collection muss "Users" oder "Any" als Read-Berechtigung haben.'
|
||||||
|
}
|
||||||
|
|
||||||
|
setState({ ref, status: 'error', ticket: null, error: errorMessage })
|
||||||
|
console.error('Error fetching workorder:', err)
|
||||||
|
}
|
||||||
|
}, [ref])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load()
|
||||||
|
// Wechselt die Adresse oder verschwindet die Seite, zaehlt eine laufende Anfrage nicht mehr
|
||||||
|
const activeGuard = guard.current
|
||||||
|
return () => { activeGuard.next() }
|
||||||
|
}, [load])
|
||||||
|
|
||||||
|
const updateWorkorder = useCallback(async (id, data) => {
|
||||||
|
try {
|
||||||
|
const response = await databases.updateDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
COLLECTIONS.WORKORDERS,
|
||||||
|
id,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
// Nur das geladene Ticket ersetzen (nicht, wenn die Seite inzwischen ein anderes zeigt)
|
||||||
|
setState((prev) => (
|
||||||
|
prev.status === 'ok' && prev.ticket?.$id === id ? { ...prev, ticket: response } : prev
|
||||||
|
))
|
||||||
|
return { success: true, data: response }
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, error: err.message }
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
ticket: current.ticket,
|
||||||
|
loading: current.status === 'loading',
|
||||||
|
error: current.error,
|
||||||
|
notFound: current.status === 'notfound',
|
||||||
|
updateWorkorder,
|
||||||
|
refresh: load,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
||||||
import { databases, DATABASE_ID, COLLECTIONS, Query, ID } from '../lib/appwrite'
|
import { databases, DATABASE_ID, COLLECTIONS, Query, ID } from '../lib/appwrite'
|
||||||
import { ticketMatches, NORMAL_PRIORITY } from '../lib/ticketStatus'
|
import { ticketMatches, NORMAL_PRIORITY } from '../lib/ticketStatus'
|
||||||
|
import { createRequestGuard } from '../lib/requestGuard'
|
||||||
|
|
||||||
function buildFiltersKey(filters = {}) {
|
function buildFiltersKey(filters = {}) {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
@@ -20,6 +21,8 @@ export function useWorkorders(filters = {}) {
|
|||||||
const [workorders, setWorkorders] = useState([])
|
const [workorders, setWorkorders] = useState([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
|
// Nur die juengste Anfrage darf das Ergebnis uebernehmen
|
||||||
|
const guard = useRef(createRequestGuard())
|
||||||
const filtersKey = useMemo(() => buildFiltersKey(filters), [
|
const filtersKey = useMemo(() => buildFiltersKey(filters), [
|
||||||
filters.limit,
|
filters.limit,
|
||||||
filters.customerId,
|
filters.customerId,
|
||||||
@@ -34,6 +37,7 @@ export function useWorkorders(filters = {}) {
|
|||||||
|
|
||||||
const fetchWorkorders = useCallback(async () => {
|
const fetchWorkorders = useCallback(async () => {
|
||||||
const activeFilters = JSON.parse(filtersKey)
|
const activeFilters = JSON.parse(filtersKey)
|
||||||
|
const requestId = guard.current.next()
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -92,6 +96,7 @@ export function useWorkorders(filters = {}) {
|
|||||||
COLLECTIONS.WORKORDERS,
|
COLLECTIONS.WORKORDERS,
|
||||||
queries
|
queries
|
||||||
)
|
)
|
||||||
|
if (!guard.current.isCurrent(requestId)) return
|
||||||
|
|
||||||
setWorkorders(
|
setWorkorders(
|
||||||
activeFilters.search
|
activeFilters.search
|
||||||
@@ -100,6 +105,7 @@ export function useWorkorders(filters = {}) {
|
|||||||
)
|
)
|
||||||
setError(null)
|
setError(null)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (!guard.current.isCurrent(requestId)) return
|
||||||
let errorMessage = err.message || 'Fehler beim Laden der Tickets'
|
let errorMessage = err.message || 'Fehler beim Laden der Tickets'
|
||||||
|
|
||||||
// Bessere Fehlermeldungen
|
// Bessere Fehlermeldungen
|
||||||
@@ -112,7 +118,7 @@ export function useWorkorders(filters = {}) {
|
|||||||
setError(errorMessage)
|
setError(errorMessage)
|
||||||
console.error('Error fetching workorders:', err)
|
console.error('Error fetching workorders:', err)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
if (guard.current.isCurrent(requestId)) setLoading(false)
|
||||||
}
|
}
|
||||||
}, [filtersKey])
|
}, [filtersKey])
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user