import { useState, useEffect, useMemo, useCallback } from 'react' import { Link, useNavigate, useParams } from 'react-router-dom' import { FaSpinner, FaPlus, FaArrowLeft, FaArrowRight, FaCheck } from 'react-icons/fa6' import { useCustomers } from '../hooks/useCustomers' import { useAuth } from '../context/AuthContext' import { databases, DATABASE_ID, COLLECTIONS, Query } from '../lib/appwrite' import { fetchServices, completeAssignment, fetchProject } from '../lib/projectKindsApi' import { nextStep, prevStep, newTicketDefaults, canFinish, openTicketsFor } from '../lib/assignFlow' import { suggestSubdomain, slugify } from '../lib/subdomain' import { isArchived } from '../lib/leads' import { ticketPath, ticketRef } from '../lib/ticketRoutes' import { PageHeader, Card, Button, Field } from '../components/ui' import { CreateCustomerForm } from './CustomersPage' const todayIso = () => new Date().toLocaleDateString('sv-SE', { timeZone: 'Europe/Berlin' }) const fmtDate = (iso) => (iso ? new Date(iso).toLocaleDateString('de-DE') : '') const STEP_TITLES = { kind: 'Was ist das?', 'customer?': 'Ist das für einen Kunden?', customer: 'Welcher Kunde?', ticket: 'Welches Ticket?', summary: 'Zusammenfassung', } export default function AssignProjectPage() { const { id } = useParams() const navigate = useNavigate() const { isAdmin } = useAuth() const { customers, createCustomer } = useCustomers() const [project, setProject] = useState(null) const [kinds, setKinds] = useState([]) const [loading, setLoading] = useState(true) const [loadError, setLoadError] = useState(null) const [notice, setNotice] = useState(null) const [done, setDone] = useState(null) // { warnings, path } nach Erfolg mit Warnungen const [state, setState] = useState({ step: 'kind', kind: '', forCustomer: false, customerId: '', ticketId: '', newTicket: null, }) const [search, setSearch] = useState('') const [showNewCustomer, setShowNewCustomer] = useState(false) const [tickets, setTickets] = useState([]) const [ticketsLoading, setTicketsLoading] = useState(false) const [ticketsError, setTicketsError] = useState(false) const [ticketsReload, setTicketsReload] = useState(0) const [previewOn, setPreviewOn] = useState(true) const [subdomain, setSubdomain] = useState('') const [busy, setBusy] = useState(false) const [error, setError] = useState(null) const load = useCallback(async () => { setLoading(true) setLoadError(null) try { const [p, services] = await Promise.all([fetchProject(id), fetchServices()]) setProject(p) setKinds(services.kinds || []) } catch (err) { setLoadError(err.message || 'Projekt konnte nicht geladen werden.') } finally { setLoading(false) } }, [id]) useEffect(() => { load() }, [load]) const repoName = String(project?.repoFullName || project?.projectName || '').split('/').pop() const repoUrl = project?.giteaRepoUrl || '' const kindObj = kinds.find((k) => k.key === state.kind) const customer = (customers || []).find((c) => c.$id === state.customerId) // Offene Tickets des Kunden laden, sobald Schritt 4 erreicht ist useEffect(() => { if (state.step !== 'ticket' || !state.customerId) return let cancelled = false setTicketsLoading(true) setTicketsError(false) databases .listDocuments(DATABASE_ID, COLLECTIONS.WORKORDERS, [ Query.equal('customerId', state.customerId), Query.notEqual('status', 'Closed'), Query.notEqual('status', 'Cancelled'), Query.orderDesc('$createdAt'), Query.limit(200), ]) .then((r) => { if (!cancelled) setTickets(r.documents || []) }) .catch(() => { if (!cancelled) { setTickets([]); setTicketsError(true) } }) .finally(() => { if (!cancelled) setTicketsLoading(false) }) return () => { cancelled = true } }, [state.step, state.customerId, ticketsReload]) const openTickets = useMemo(() => openTicketsFor(tickets, state.customerId), [tickets, state.customerId]) const isWebsiteWithCustomer = state.kind === 'website' && state.forCustomer // Hat das Projekt schon eine Website (z. B. ueber "Neues Projekt" mit Subdomain angelegt), bleibt // sie wie sie ist: kein Vorschlag, kein erneutes Einschalten (das benannte sie sonst um und // stellte eine oeffentliche Website auf Login-Preview). const hasPreviewAlready = Boolean(project?.subdomain) useEffect(() => { if (state.step === 'summary' && isWebsiteWithCustomer && !hasPreviewAlready && !subdomain) { setSubdomain(suggestSubdomain('', repoName)) } }, [state.step, isWebsiteWithCustomer, hasPreviewAlready, subdomain, repoName]) const patch = (p) => setState((s) => ({ ...s, ...p })) const go = (step) => { setError(null); patch({ step }) } const startNewTicket = () => { patch({ ticketId: '', newTicket: newTicketDefaults({ kindLabel: kindObj?.label || '', ticketType: kindObj?.ticketType || 'Webpage', repoName, repoUrl, customer, today: todayIso(), }), }) } const setNT = (field) => (e) => setState((s) => ({ ...s, newTicket: { ...s.newTicket, [field]: e.target.value } })) const stepReady = () => { switch (state.step) { case 'kind': return !!state.kind case 'customer': return !!state.customerId case 'ticket': return canFinish(state) default: return false } } const finish = async () => { setBusy(true) setError(null) // R9: nie newTicket.type senden, genau eins von ticketId / newTicket const body = { kind: state.kind, forCustomer: state.forCustomer } if (state.forCustomer) { body.customerId = state.customerId if (state.ticketId) { body.ticketId = state.ticketId } else { const { topic, requestedBy, startDate, details } = state.newTicket body.newTicket = { topic, requestedBy, startDate, details } } if (isWebsiteWithCustomer && !hasPreviewAlready && previewOn && subdomain.trim()) { body.preview = { subdomain: slugify(subdomain) } } } try { const result = await completeAssignment(id, body) let path = state.forCustomer ? '/projects' : '/services' if (result.ticketId) { try { const t = await databases.getDocument(DATABASE_ID, COLLECTIONS.WORKORDERS, result.ticketId) path = ticketPath(ticketRef(t)) } catch { path = '/tickets' } } if (result.warnings?.length) { setDone({ warnings: result.warnings, path }) setBusy(false) } else { navigate(path) } } catch (err) { if (err.status === 409) { setNotice('Die Art wurde inzwischen von jemand anderem festgelegt.') await load() } else { setError(err.message || 'Abschließen fehlgeschlagen.') } setBusy(false) } } if (loading) return
Bereits erledigt von {project.kindSetBy || 'unbekannt'} am {fmtDate(project.kindSetAt)}.
{project.ticketId && ({['deployed', 'ready'].includes(String(project.status || '').toLowerCase()) ? <>Preview läuft bereits unter https://{project.subdomain}.project.webklar.com und bleibt unverändert.> : <>Preview ist unter https://{project.subdomain}.project.webklar.com eingerichtet (Status: {project.status || 'unbekannt'}). Neu bauen geht im Projekt über „Preview".>}
)} {isWebsiteWithCustomer && !hasPreviewAlready && (