diff --git a/src/components/AnalyseRow.jsx b/src/components/AnalyseRow.jsx new file mode 100644 index 0000000..096976e --- /dev/null +++ b/src/components/AnalyseRow.jsx @@ -0,0 +1,83 @@ +import { useState, useEffect } from 'react' +import { fetchAnalyse, setAnalyse } from '../lib/analyseApi' +import { analyseStatus, avvLabel } from '../lib/analyseView' + +const COLORS = { green: '#10b981', red: '#ef4444', grey: '#a0aec0' } + +// Kompakte Zeile "Website-Analyse" je Projekt (lädt beim Einbinden; mit key=Projekt-ID verwenden) +export default function AnalyseRow({ projectId }) { + const [data, setData] = useState(null) + const [loadError, setLoadError] = useState('') + const [mode, setMode] = useState(null) // null | 'on' | 'off' + const [confirmed, setConfirmed] = useState(false) + const [busy, setBusy] = useState(false) + const [error, setError] = useState('') + + useEffect(() => { + let cancelled = false + fetchAnalyse(projectId) + .then((r) => { if (!cancelled) setData(r) }) + .catch((err) => { if (!cancelled) setLoadError(err.message) }) + return () => { cancelled = true } + }, [projectId]) + + const apply = async (on) => { + setBusy(true) + setError('') + try { + await setAnalyse(projectId, { on, avvConfirmedVerbally: on }) + setData(await fetchAnalyse(projectId)) + setMode(null) + setConfirmed(false) + } catch (err) { + setError(err.message) + } finally { + setBusy(false) + } + } + + const cancel = () => { setMode(null); setConfirmed(false); setError('') } + const status = analyseStatus(data) + + return ( +
+
+ Website-Analyse: + {loadError ? ( + {loadError} + ) : data ? ( + {status.text} + ) : ( + Lädt … + )} + {status.canEnable && !mode && ( + + )} + {status.canDisable && !mode && ( + + )} +
+ + {mode === 'on' && data && ( +
+ + + +
+ )} + + {mode === 'off' && ( +
+ Website-Analyse wirklich ausschalten? + + +
+ )} + + {error &&
{error}
} +
+ ) +} diff --git a/src/components/AssignedProjectCard.jsx b/src/components/AssignedProjectCard.jsx index b68023a..923d485 100644 --- a/src/components/AssignedProjectCard.jsx +++ b/src/components/AssignedProjectCard.jsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { FaCodeBranch } from 'react-icons/fa' import { FaLinkSlash } from 'react-icons/fa6' import PreviewBlock from './PreviewBlock' +import AnalyseRow from './AnalyseRow' import GiteaLinkButton from './GiteaLinkButton' import PreviewLinkButton from './PreviewLinkButton' import { suggestSubdomain } from '../lib/subdomain' @@ -112,6 +113,8 @@ export default function AssignedProjectCard({ project, onUnassign, showActions = + {project.subdomain && } + {showActions && project.subdomain && (
{[p.customerName, p.previewUrl && 'Preview'].filter(Boolean).join(' · ') || 'Intern'} diff --git a/tests/unit/analyseView.test.mjs b/tests/unit/analyseView.test.mjs new file mode 100644 index 0000000..5e19bbb --- /dev/null +++ b/tests/unit/analyseView.test.mjs @@ -0,0 +1,49 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { loadModule } from './load.mjs' + +const { analyseStatus, avvLabel } = await loadModule('src/lib/analyseView.js') + +test('analyseStatus: Schalter aus => noch nicht freigeschaltet, kein Button', () => { + const s = analyseStatus({ enabled: false }) + assert.equal(s.text, 'Noch nicht freigeschaltet') + assert.equal(s.canEnable, false) + assert.equal(s.canDisable, false) +}) + +test('analyseStatus: an seit Datum (Europe/Berlin)', () => { + const s = analyseStatus({ enabled: true, state: 'active', eligible: true, activatedAt: '2026-10-01T22:30:00Z' }) + assert.equal(s.text, 'An seit 02.10.2026') + assert.equal(s.canDisable, true) + assert.equal(s.canEnable, false) +}) + +test('analyseStatus: an ohne Datum bleibt lesbar', () => { + assert.equal(analyseStatus({ enabled: true, state: 'active', eligible: true }).text, 'An') +}) + +test('analyseStatus: aus und berechtigt => Einschalten moeglich', () => { + const s = analyseStatus({ enabled: true, state: 'off', eligible: true }) + assert.equal(s.text, 'Aus') + assert.equal(s.canEnable, true) + assert.equal(analyseStatus({ enabled: true, state: null, eligible: true }).text, 'Aus') +}) + +test('analyseStatus: gesperrt', () => { + const s = analyseStatus({ enabled: true, state: 'disabled', eligible: true }) + assert.equal(s.text, 'Gesperrt') +}) + +test('analyseStatus: nicht berechtigt => Grund, kein Button', () => { + const s = analyseStatus({ enabled: true, state: null, eligible: false, reasonText: 'Die Website-Analyse gibt es nur mit einem laufenden Wartungs-Abo.' }) + assert.equal(s.text, 'Nicht verfügbar: Die Website-Analyse gibt es nur mit einem laufenden Wartungs-Abo.') + assert.equal(s.canEnable, false) +}) + +test('analyseStatus: robust bei leerer Antwort', () => { + assert.equal(analyseStatus(null).text, '') +}) + +test('avvLabel nennt die Fassung', () => { + assert.equal(avvLabel('2026-10-01'), 'Der Kunde hat den Auftragsverarbeitungsvertrag (Fassung 2026-10-01) bestätigt') +}) diff --git a/tests/unit/servicesView.test.mjs b/tests/unit/servicesView.test.mjs index ef9762e..513b285 100644 --- a/tests/unit/servicesView.test.mjs +++ b/tests/unit/servicesView.test.mjs @@ -38,3 +38,8 @@ test('servicesCards: robust bei leerer Antwort und unbekannter Funktion', () => const cards = servicesCards({ kinds: [{ key: 'x', label: 'X', features: ['foo'] }] }) assert.deepEqual(cards[0].features, [{ label: 'foo', statusLabel: '' }]) }) + +test('servicesCards: Status "available" => "Verfügbar"', () => { + const cards = servicesCards({ kinds, features: { analyse: { label: 'Website-Analyse', status: 'available' } }, groups: {} }) + assert.deepEqual(cards[0].features, [{ label: 'Website-Analyse', statusLabel: 'Verfügbar' }]) +})