import { useState, useEffect, useCallback } from 'react' import { FaPlus, FaLink } from 'react-icons/fa6' import { useWebsiteProjects } from '../hooks/useWebsiteProjects' import { createProjectFromTemplate, importNewReposThrottled } from '../lib/projectAdminApi' import { isOutsideOrg, assignSummary, assignableProjects } from '../lib/projectAssign' import { slugify } from '../lib/subdomain' import AssignedProjectCard from './AssignedProjectCard' import { fetchServices, completeAssignment } from '../lib/projectKindsApi' import { kindLabelFor } from '../lib/assignFlow' // Projekte am Ticket: Liste, Zuweisen, Loesen und neues Projekt an einer Stelle. // Loesen nimmt das Projekt nur vom Ticket; die Kundenzuordnung (Sichtbarkeit im Portal) bleibt. export default function TicketProjects({ ticket }) { const { fetchAllProjects, fetchByTicketId, assignProjects, unassignProject } = useWebsiteProjects() const [assigned, setAssigned] = useState([]) const [available, setAvailable] = useState([]) const [selected, setSelected] = useState('') const [showCreate, setShowCreate] = useState(false) const [createForm, setCreateForm] = useState({ displayName: '', subdomain: '' }) const [busy, setBusy] = useState(false) const [error, setError] = useState('') const [importing, setImporting] = useState(false) const [importHint, setImportHint] = useState('') const [assignResult, setAssignResult] = useState(null) const [kinds, setKinds] = useState([]) const [kindDialog, setKindDialog] = useState(false) useEffect(() => { fetchServices().then((s) => setKinds(s.kinds || [])).catch(() => {}) }, []) const load = useCallback(async () => { if (!ticket?.$id) return const [all, mine] = await Promise.all([fetchAllProjects(), fetchByTicketId(ticket.$id)]) setAssigned(mine) setAvailable(assignableProjects(all, ticket, new Set(mine.map((p) => p.$id)))) }, [ticket, fetchAllProjects, fetchByTicketId]) useEffect(() => { let cancelled = false ;(async () => { setImporting(true) setImportHint('') try { await importNewReposThrottled() } catch (err) { if (!cancelled) setImportHint(`Neue Repos konnten nicht geladen werden (${err.message}).`) } finally { if (!cancelled) setImporting(false) } if (!cancelled) await load() })() return () => { cancelled = true } }, [load]) const run = async (action) => { setBusy(true) setError('') try { await action() } catch (err) { setError(err.message || 'Aktion fehlgeschlagen') } finally { // Auch nach einem Fehler neu laden: das Repo kann trotzdem entstanden sein try { await load() } catch { /* Anzeige bleibt wie sie ist */ } setBusy(false) } } const assign = () => { const project = available.find((p) => p.$id === selected) // Ohne Art erst "Was ist das?" fragen if (project && !project.kind) { setKindDialog(true) return } assignPlain() } const assignPlain = async () => { setBusy(true) setError('') setAssignResult(null) try { const projectName = available.find((p) => p.$id === selected)?.projectName || '' const r = await assignProjects([selected], { customerId: ticket.customerId || '', ticketId: ticket.$id }) const one = r.results[0] setAssignResult(assignSummary(projectName, one.ok ? one.result : { error: one.error })) if (r.success) setSelected('') await load() } catch (err) { setError(err.message || 'Aktion fehlgeschlagen') } finally { setBusy(false) } } // Art gewaehlt: completeAssignment weist serverseitig (wie assignProject) zu, // daher kein zusaetzliches assignProjects. // Bei 409 (bereits zugeordnet) bewusst NICHT auf assignProjects zurueckfallen: // das wuerde das Projekt ohne Art zuweisen. Stattdessen Meldung zeigen und neu laden. const assignWithKind = async (kind) => { setKindDialog(false) setError('') setAssignResult(null) if (!ticket.customerId) { setError('Dieses Ticket hat keinen Kunden. Bitte zuerst einen Kunden am Ticket eintragen.') return } setBusy(true) try { const projectName = available.find((p) => p.$id === selected)?.projectName || '' const r = await completeAssignment(selected, { kind, forCustomer: true, customerId: ticket.customerId, ticketId: ticket.$id, }) setAssignResult({ text: `${projectName || 'Projekt'} wurde zugewiesen.`, warnings: r.warnings || [] }) setSelected('') } catch (err) { setError(err.status === 409 ? `${err.message} Die Liste wurde neu geladen.` : err.message || 'Aktion fehlgeschlagen') } finally { try { await load() } catch { /* Anzeige bleibt wie sie ist */ } setBusy(false) } } const unassign = (projectId) => run(async () => { const r = await unassignProject(projectId) if (!r.success) throw new Error(r.error) }) const create = (e) => { e.preventDefault() if (!slugify(createForm.subdomain)) { setError('Bitte eine gültige Subdomain eingeben (Buchstaben, Ziffern, Bindestrich).') return } run(async () => { const subdomain = slugify(createForm.subdomain) await createProjectFromTemplate({ repoName: subdomain, displayName: createForm.displayName, subdomain, customerId: ticket.customerId || '', ticketId: ticket.$id, // Aus der Website-Vorlage mit Subdomain: die Art steht fest (sonst "Art fehlt") kind: 'website', }) setCreateForm({ displayName: '', subdomain: '' }) setShowCreate(false) }) } return (
Suche neue Repos …
} {importHint &&Noch kein Projekt an diesem Ticket.
: assigned.map((p) => (Bitte wähle die Art des Projekts, bevor es zugewiesen wird.
{kinds.length === 0 && (