diff --git a/src/lib/assignFlow.js b/src/lib/assignFlow.js new file mode 100644 index 0000000..ced98e7 --- /dev/null +++ b/src/lib/assignFlow.js @@ -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' + ) +} diff --git a/src/lib/projectKindsApi.js b/src/lib/projectKindsApi.js new file mode 100644 index 0000000..3907ac4 --- /dev/null +++ b/src/lib/projectKindsApi.js @@ -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) +} diff --git a/tests/unit/assignFlow.test.mjs b/tests/unit/assignFlow.test.mjs new file mode 100644 index 0000000..09bfa30 --- /dev/null +++ b/tests/unit/assignFlow.test.mjs @@ -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'), []) +})