Tickets: Zeit-Spalte (Bloecke, 3 parallel, 200 Zeilen), Ticket-Suche per woid oder ID, Anfrage-Zaehler
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9fec381056
commit
0b53fc825a
6 files changed
+201
No files matched your search
@@ -0,0 +1,14 @@
|
||||
/* Nur die juengste Anfrage darf das Ergebnis uebernehmen (spaete, aeltere Antworten werden verworfen) */
|
||||
|
||||
export function createRequestGuard() {
|
||||
let current = 0
|
||||
return {
|
||||
next() {
|
||||
current += 1
|
||||
return current
|
||||
},
|
||||
isCurrent(id) {
|
||||
return id === current
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/* Spalte "Zeit" der Ticketliste: Arbeitszeit je Ticket, in Bloecken nachgeladen.
|
||||
Nur Lesezugriffe. Eine Zelle ohne Ergebnis (laedt noch, Fehler, 0 min, abgeschnitten,
|
||||
jenseits der ersten 200 Zeilen) zeigt immer "–", nie eine falsche Zahl. */
|
||||
|
||||
import { sumWorksheetMinutes } from './worksheetView'
|
||||
import { stepLabel } from './timeSteps'
|
||||
|
||||
export const TIME_BLOCK = 100 // woids je Abfrage
|
||||
export const TIME_QUERY_LIMIT = 5000 // Dokumente je Abfrage
|
||||
export const TIME_MAX_ROWS = 200 // nur die ersten Zeilen bekommen eine Zeit
|
||||
export const TIME_PARALLEL = 3 // gleichzeitige Abfragen
|
||||
|
||||
export function chunk(list, size) {
|
||||
const out = []
|
||||
for (let i = 0; i < list.length; i += size) out.push(list.slice(i, i + size))
|
||||
return out
|
||||
}
|
||||
|
||||
// Welche woids sind noch zu laden? Erste 200 Zeilen, ohne leere, doppelte und schon bekannte.
|
||||
export function planTimeLoad(tickets, known = new Set()) {
|
||||
const list = tickets || []
|
||||
const seen = new Set()
|
||||
const woids = []
|
||||
for (const ticket of list.slice(0, TIME_MAX_ROWS)) {
|
||||
const woid = String(ticket?.woid ?? '').trim()
|
||||
if (!woid || seen.has(woid) || known.has(woid)) continue
|
||||
seen.add(woid)
|
||||
woids.push(woid)
|
||||
}
|
||||
return { blocks: chunk(woids, TIME_BLOCK), capped: list.length > TIME_MAX_ROWS }
|
||||
}
|
||||
|
||||
// Minuten je woid eines Blocks. Wurde die Antwort abgeschnitten (Limit erreicht oder
|
||||
// mehr Treffer als geliefert), ist keine Summe verlaesslich: null.
|
||||
export function blockMinutes(woids, documents, total, limit = TIME_QUERY_LIMIT) {
|
||||
const docs = documents || []
|
||||
if (docs.length >= limit) return null
|
||||
if (typeof total === 'number' && total > docs.length) return null
|
||||
const byWoid = new Map()
|
||||
for (const doc of docs) {
|
||||
const key = String(doc?.woid ?? '').trim()
|
||||
if (!byWoid.has(key)) byWoid.set(key, [])
|
||||
byWoid.get(key).push(doc)
|
||||
}
|
||||
return Object.fromEntries(woids.map((woid) => [woid, sumWorksheetMinutes(byWoid.get(woid) || [])]))
|
||||
}
|
||||
|
||||
// Worker-Pool: hoechstens `limit` gleichzeitig, Ergebnisse in Eingabereihenfolge, verwirft nie
|
||||
export async function mapLimit(items, limit, fn) {
|
||||
const results = new Array(items.length)
|
||||
let next = 0
|
||||
const worker = async () => {
|
||||
while (next < items.length) {
|
||||
const index = next
|
||||
next += 1
|
||||
try {
|
||||
results[index] = { ok: true, value: await fn(items[index], index) }
|
||||
} catch (error) {
|
||||
results[index] = { ok: false, error }
|
||||
}
|
||||
}
|
||||
}
|
||||
await Promise.all(Array.from({ length: Math.min(Math.max(1, limit), items.length) }, worker))
|
||||
return results
|
||||
}
|
||||
|
||||
export function minutesCellText(ticket, index, byWoid) {
|
||||
if (index >= TIME_MAX_ROWS) return '–'
|
||||
const woid = String(ticket?.woid ?? '').trim()
|
||||
if (!woid) return '–'
|
||||
const minutes = byWoid?.get(woid)
|
||||
if (typeof minutes !== 'number' || !(minutes > 0)) return '–'
|
||||
return stepLabel(minutes)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/* Ticket zu einer Adresse wie /tickets/<ref>: erst die Nummer (woid), dann die Dokument-ID.
|
||||
Kennt kein Appwrite, die Seite reicht die Abfragen herein. */
|
||||
|
||||
const NOT_FOUND_CODES = [400, 404]
|
||||
|
||||
export async function lookupWorkorder(ref, { findByWoid, getById }) {
|
||||
const key = String(ref ?? '').trim()
|
||||
if (!key) return { status: 'notfound' }
|
||||
|
||||
const byWoid = await findByWoid(key)
|
||||
if (byWoid) return { status: 'ok', ticket: byWoid }
|
||||
|
||||
try {
|
||||
const byId = await getById(key)
|
||||
return byId ? { status: 'ok', ticket: byId } : { status: 'notfound' }
|
||||
} catch (error) {
|
||||
// Appwrite: 404 = keine solche ID, 400 = keine gueltige ID
|
||||
if (NOT_FOUND_CODES.includes(Number(error?.code))) return { status: 'notfound' }
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { loadModule } from './load.mjs'
|
||||
|
||||
const { createRequestGuard } = await loadModule('src/lib/requestGuard.js')
|
||||
test('nur die jüngste Anfrage gilt', () => {
|
||||
const g = createRequestGuard()
|
||||
const a = g.next(); const b = g.next()
|
||||
assert.equal(g.isCurrent(a), false)
|
||||
assert.equal(g.isCurrent(b), true)
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { loadModule } from './load.mjs'
|
||||
|
||||
const t = await loadModule('src/lib/ticketTimes.js')
|
||||
const tickets = (n, from = 100000) => Array.from({ length: n }, (_, i) => ({ $id: 'id' + i, woid: String(from + i) }))
|
||||
const sup = JSON.stringify([{ employeeId: 'e', name: 'N', task: 'x' }])
|
||||
test('chunk und planTimeLoad: erste 200 Zeilen, Blöcke zu 100', () => {
|
||||
assert.deepEqual(t.chunk([1, 2, 3, 4, 5], 2), [[1, 2], [3, 4], [5]])
|
||||
const plan = t.planTimeLoad(tickets(250), new Set())
|
||||
assert.deepEqual(plan.blocks.map((b) => b.length), [100, 100])
|
||||
assert.equal(plan.blocks[1].at(-1), '100199')
|
||||
assert.equal(plan.capped, true)
|
||||
assert.equal(t.planTimeLoad(tickets(200), new Set()).capped, false)
|
||||
})
|
||||
test('planTimeLoad: nur Neues, ohne woid, ohne Doppelte', () => {
|
||||
const known = new Set(tickets(100).map((x) => x.woid))
|
||||
assert.deepEqual(t.planTimeLoad(tickets(120), known).blocks, [tickets(20, 100100).map((x) => x.woid)])
|
||||
assert.deepEqual(t.planTimeLoad([{ $id: 'a' }, { woid: '' }, { woid: '5' }, { woid: 5 }, { woid: '5' }], new Set()).blocks, [['5']])
|
||||
assert.deepEqual(t.planTimeLoad(tickets(3), new Set(tickets(3).map((x) => x.woid))).blocks, [])
|
||||
})
|
||||
test('blockMinutes: Summe ohne Kommentare, mit Unterstützung, nur woids des Blocks', () => {
|
||||
const docs = [
|
||||
{ woid: '1', totalTime: 60 }, { woid: '1', totalTime: 30, supporters: sup }, { woid: '1', totalTime: 0, isComment: true },
|
||||
{ woid: '2', totalTime: 45, isComment: true }, { woid: 7, totalTime: 15 }, { woid: '99', totalTime: 15 },
|
||||
]
|
||||
assert.deepEqual(t.blockMinutes(['1', '2', '3', '7'], docs, 6), { 1: 105, 2: 0, 3: 0, 7: 15 })
|
||||
})
|
||||
test('blockMinutes: abgeschnittene Blöcke liefern null, nie eine falsche Zahl', () => {
|
||||
assert.equal(t.blockMinutes(['1'], Array.from({ length: 5000 }, () => ({ woid: '1', totalTime: 15 })), 5000), null)
|
||||
assert.equal(t.blockMinutes(['1'], [{ woid: '1', totalTime: 15 }], 7), null)
|
||||
assert.deepEqual(t.blockMinutes(['1'], [{ woid: '1', totalTime: 15 }], 1), { 1: 15 })
|
||||
})
|
||||
test('minutesCellText: Fehler, null, 0, ohne woid und jenseits von 200 zeigen –', () => {
|
||||
const by = new Map([['1', 105], ['2', 0], ['3', null]])
|
||||
assert.equal(t.minutesCellText({ woid: '1' }, 0, by), '1 h 45 min')
|
||||
for (const x of [{ woid: '2' }, { woid: '3' }, { woid: '4' }, {}, { woid: '' }]) assert.equal(t.minutesCellText(x, 0, by), '–')
|
||||
assert.equal(t.minutesCellText({ woid: '1' }, 199, by), '1 h 45 min')
|
||||
assert.equal(t.minutesCellText({ woid: '1' }, 200, by), '–')
|
||||
})
|
||||
test('mapLimit: höchstens 3 gleichzeitig, Reihenfolge, Fehler je Element', async () => {
|
||||
let running = 0, peak = 0
|
||||
const out = await t.mapLimit([1, 2, 3, 4, 5, 6, 7], 3, async (n) => {
|
||||
running += 1; peak = Math.max(peak, running)
|
||||
await new Promise((r) => setTimeout(r, 5))
|
||||
running -= 1
|
||||
if (n === 4) throw new Error('x')
|
||||
return n * 2
|
||||
})
|
||||
assert.equal(peak, 3)
|
||||
assert.deepEqual(out.map((r) => r.ok), [true, true, true, false, true, true, true])
|
||||
assert.deepEqual(out.filter((r) => r.ok).map((r) => r.value), [2, 4, 6, 10, 12, 14])
|
||||
assert.deepEqual(await t.mapLimit([], 3, async () => 1), [])
|
||||
})
|
||||
@@ -0,0 +1,27 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { loadModule } from './load.mjs'
|
||||
|
||||
const { lookupWorkorder } = await loadModule('src/lib/workorderLookup.js')
|
||||
const fake = ({ byWoid = null, byId, byIdError } = {}) => {
|
||||
const calls = []
|
||||
return { calls, findByWoid: async (w) => { calls.push(['woid', w]); return byWoid }, getById: async (id) => { calls.push(['id', id]); if (byIdError) throw byIdError; return byId } }
|
||||
}
|
||||
test('erst woid, dann $id', async () => {
|
||||
let d = fake({ byWoid: { $id: 'a', woid: '5' } })
|
||||
assert.deepEqual(await lookupWorkorder(' 5 ', d), { status: 'ok', ticket: { $id: 'a', woid: '5' } })
|
||||
assert.deepEqual(d.calls, [['woid', '5']])
|
||||
d = fake({ byId: { $id: 'abc' } })
|
||||
assert.deepEqual(await lookupWorkorder('abc', d), { status: 'ok', ticket: { $id: 'abc' } })
|
||||
assert.deepEqual(d.calls, [['woid', 'abc'], ['id', 'abc']])
|
||||
})
|
||||
test('400 und 404 sind nicht gefunden, leeres ref ohne Abfrage', async () => {
|
||||
for (const code of [400, 404]) assert.deepEqual(await lookupWorkorder('x', fake({ byIdError: { code } })), { status: 'notfound' })
|
||||
const d = fake()
|
||||
for (const empty of ['', ' ', null, undefined]) assert.deepEqual(await lookupWorkorder(empty, d), { status: 'notfound' })
|
||||
assert.deepEqual(d.calls, [])
|
||||
})
|
||||
test('andere Fehler werden weitergereicht', async () => {
|
||||
await assert.rejects(lookupWorkorder('x', fake({ byIdError: { code: 500, message: 'boom' } })))
|
||||
await assert.rejects(lookupWorkorder('x', { findByWoid: async () => { throw new Error('netz') }, getById: async () => ({}) }), /netz/)
|
||||
})
|
||||
Reference in new issue
Block a user