Projektarten: Ablauf-Logik und API-Client
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
24e4f054c4
commit
9dc447e92c
3 files changed
+170
No files matched your search
@@ -0,0 +1,57 @@
|
|||||||
|
// Ablauf-Logik des Zuordnen-Assistenten (reine Funktionen, ohne UI).
|
||||||
|
// state: { step, kind, forCustomer, customerId, ticketId, newTicket }
|
||||||
|
|
||||||
|
export function nextStep(state) {
|
||||||
|
switch (state.step) {
|
||||||
|
case 'kind':
|
||||||
|
return 'customer?'
|
||||||
|
case 'customer?':
|
||||||
|
return state.forCustomer ? 'customer' : 'summary'
|
||||||
|
case 'customer':
|
||||||
|
return 'ticket'
|
||||||
|
default:
|
||||||
|
return 'summary'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prevStep(state) {
|
||||||
|
switch (state.step) {
|
||||||
|
case 'customer?':
|
||||||
|
return 'kind'
|
||||||
|
case 'customer':
|
||||||
|
return 'customer?'
|
||||||
|
case 'ticket':
|
||||||
|
return 'customer'
|
||||||
|
case 'summary':
|
||||||
|
return state.forCustomer ? 'ticket' : 'customer?'
|
||||||
|
default:
|
||||||
|
return 'kind'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function newTicketDefaults({ kindLabel, ticketType, repoName, repoUrl, customer, today }) {
|
||||||
|
return {
|
||||||
|
topic: `${kindLabel} – ${repoName}`,
|
||||||
|
type: ticketType,
|
||||||
|
requestedBy: customer?.contactName || customer?.name || '',
|
||||||
|
startDate: today,
|
||||||
|
details: `Repository: ${repoUrl}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filled = (v) => typeof v === 'string' && v.trim() !== ''
|
||||||
|
|
||||||
|
export function canFinish(state) {
|
||||||
|
if (!state.kind) return false
|
||||||
|
if (!state.forCustomer) return true
|
||||||
|
if (!state.customerId) return false
|
||||||
|
if (state.ticketId) return true
|
||||||
|
const t = state.newTicket
|
||||||
|
return !!t && filled(t.topic) && filled(t.requestedBy) && filled(t.startDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openTicketsFor(tickets, customerId) {
|
||||||
|
return (tickets || []).filter(
|
||||||
|
(t) => t.customerId === customerId && t.status !== 'Closed' && t.status !== 'Cancelled'
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { account, databases, DATABASE_ID, COLLECTIONS } from './appwrite'
|
||||||
|
|
||||||
|
const PROJECT_ADMIN_URL =
|
||||||
|
import.meta.env.VITE_PROJECT_ADMIN_URL || 'https://project.webklar.com'
|
||||||
|
|
||||||
|
// Wie adminFetch in projectAdminApi.js, aber der Fehler trägt .status und .data
|
||||||
|
// (Antwort-Body), damit die Seite z. B. die Details eines 409 anzeigen kann.
|
||||||
|
async function kindsFetch(path, options = {}) {
|
||||||
|
const jwt = await account.createJWT()
|
||||||
|
const response = await fetch(`${PROJECT_ADMIN_URL}${path}`, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwt.jwt}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const data = await response.json().catch(() => ({}))
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = new Error(data.error || `API-Fehler ${response.status}`)
|
||||||
|
error.status = response.status
|
||||||
|
error.data = data
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchServices() {
|
||||||
|
return kindsFetch('/api/admin/services')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function completeAssignment(projectId, body) {
|
||||||
|
return kindsFetch(`/api/admin/website-projects/${projectId}/zuordnen`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchProject(projectId) {
|
||||||
|
return databases.getDocument(DATABASE_ID, COLLECTIONS.WEBSITE_PROJECTS, projectId)
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { loadModule } from './load.mjs'
|
||||||
|
|
||||||
|
const { nextStep, prevStep, newTicketDefaults, canFinish, openTicketsFor } = await loadModule(
|
||||||
|
'src/lib/assignFlow.js'
|
||||||
|
)
|
||||||
|
|
||||||
|
test('nextStep: Kunde ja durchläuft alle Schritte', () => {
|
||||||
|
const s = { forCustomer: true }
|
||||||
|
assert.equal(nextStep({ ...s, step: 'kind' }), 'customer?')
|
||||||
|
assert.equal(nextStep({ ...s, step: 'customer?' }), 'customer')
|
||||||
|
assert.equal(nextStep({ ...s, step: 'customer' }), 'ticket')
|
||||||
|
assert.equal(nextStep({ ...s, step: 'ticket' }), 'summary')
|
||||||
|
assert.equal(nextStep({ ...s, step: 'summary' }), 'summary')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('nextStep: ohne Kunde springt customer? direkt zu summary', () => {
|
||||||
|
assert.equal(nextStep({ step: 'customer?', forCustomer: false }), 'summary')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('prevStep', () => {
|
||||||
|
assert.equal(prevStep({ step: 'kind' }), 'kind')
|
||||||
|
assert.equal(prevStep({ step: 'customer?' }), 'kind')
|
||||||
|
assert.equal(prevStep({ step: 'customer', forCustomer: true }), 'customer?')
|
||||||
|
assert.equal(prevStep({ step: 'ticket', forCustomer: true }), 'customer')
|
||||||
|
assert.equal(prevStep({ step: 'summary', forCustomer: true }), 'ticket')
|
||||||
|
assert.equal(prevStep({ step: 'summary', forCustomer: false }), 'customer?')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('newTicketDefaults', () => {
|
||||||
|
const d = newTicketDefaults({
|
||||||
|
kindLabel: 'Migration/Umzug',
|
||||||
|
ticketType: 'Migration',
|
||||||
|
repoName: 'foo',
|
||||||
|
repoUrl: 'https://git.webklar.com/webklar/foo',
|
||||||
|
customer: { name: 'Firma', contactName: 'Max' },
|
||||||
|
today: '2026-09-30',
|
||||||
|
})
|
||||||
|
assert.equal(d.topic, 'Migration/Umzug – foo')
|
||||||
|
assert.equal(d.type, 'Migration')
|
||||||
|
assert.equal(d.requestedBy, 'Max')
|
||||||
|
assert.equal(d.startDate, '2026-09-30')
|
||||||
|
assert.ok(d.details.includes('https://git.webklar.com/webklar/foo'))
|
||||||
|
const e = newTicketDefaults({ kindLabel: 'Website', ticketType: 'Webpage', repoName: 'x', repoUrl: 'u', customer: { name: 'Firma' }, today: '2026-09-30' })
|
||||||
|
assert.equal(e.requestedBy, 'Firma')
|
||||||
|
assert.equal(e.type, 'Webpage')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('canFinish', () => {
|
||||||
|
assert.equal(canFinish({ kind: '' , forCustomer: false }), false)
|
||||||
|
assert.equal(canFinish({ kind: 'app', forCustomer: false }), true)
|
||||||
|
assert.equal(canFinish({ kind: 'app', forCustomer: true }), false)
|
||||||
|
assert.equal(canFinish({ kind: 'app', forCustomer: true, customerId: 'c1' }), false)
|
||||||
|
assert.equal(canFinish({ kind: 'app', forCustomer: true, customerId: 'c1', ticketId: 't1' }), true)
|
||||||
|
const nt = { topic: 'T', requestedBy: 'M', startDate: '2026-09-30', details: 'd' }
|
||||||
|
assert.equal(canFinish({ kind: 'app', forCustomer: true, customerId: 'c1', newTicket: nt }), true)
|
||||||
|
assert.equal(canFinish({ kind: 'app', forCustomer: true, customerId: 'c1', newTicket: { ...nt, topic: ' ' } }), false)
|
||||||
|
assert.equal(canFinish({ kind: 'app', forCustomer: true, newTicket: nt }), false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('openTicketsFor', () => {
|
||||||
|
const t = [
|
||||||
|
{ $id: '1', customerId: 'c1', status: 'Open' },
|
||||||
|
{ $id: '2', customerId: 'c1', status: 'Closed' },
|
||||||
|
{ $id: '3', customerId: 'c1', status: 'Cancelled' },
|
||||||
|
{ $id: '4', customerId: 'c2', status: 'Open' },
|
||||||
|
{ $id: '5', customerId: 'c1', status: 'In Progress' },
|
||||||
|
]
|
||||||
|
assert.deepEqual(openTicketsFor(t, 'c1').map((x) => x.$id), ['1', '5'])
|
||||||
|
assert.deepEqual(openTicketsFor(null, 'c1'), [])
|
||||||
|
})
|
||||||
Reference in new issue
Block a user