Projekte: Projektabschnitt am Ticket als Abschnitt, Art-Frage als Dialog

Der Abschnitt nutzt Card, Button und Dialog statt Altklassen und Icons. TicketProjects laedt nur noch bei einer anderen Ticket-ID neu (load haengt an ticket.$id, der neueste Ticketstand kommt ueber ein ref), damit "Suche neue Repos ..." bei Status-, Zustaendig- oder Dringend-Aenderung nicht erneut blinkt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-06 14:34:29 +00:00
1 parent 47cf82eb91
commit f1aac2ada9
1 file changed
+43 -39
+43 -39
View File
@@ -1,5 +1,4 @@
import { useState, useEffect, useCallback } from 'react'
import { FaPlus, FaLink } from 'react-icons/fa6'
import { Fragment, useState, useEffect, useCallback, useRef } from 'react'
import { useWebsiteProjects } from '../hooks/useWebsiteProjects'
import { createProjectFromTemplate, importNewReposThrottled } from '../lib/projectAdminApi'
import { isOutsideOrg, assignSummary, assignableProjects } from '../lib/projectAssign'
@@ -7,6 +6,7 @@ import { slugify } from '../lib/subdomain'
import AssignedProjectCard from './AssignedProjectCard'
import { fetchServices, completeAssignment } from '../lib/projectKindsApi'
import { kindLabelFor } from '../lib/assignFlow'
import { Card, Button, Dialog } from './ui'
// 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.
@@ -29,12 +29,18 @@ export default function TicketProjects({ ticket }) {
fetchServices().then((s) => setKinds(s.kinds || [])).catch(() => {})
}, [])
// Neu laden (samt "Suche neue Repos ...") nur bei einem anderen Ticket, nicht bei Status, Zustaendig oder Dringend:
// load haengt an der Ticket-ID, den jeweils neuesten Ticketstand (Kunde) liest es ueber ticketRef
const ticketRef = useRef(ticket)
ticketRef.current = ticket
const ticketId = ticket?.$id
const load = useCallback(async () => {
if (!ticket?.$id) return
const [all, mine] = await Promise.all([fetchAllProjects(), fetchByTicketId(ticket.$id)])
if (!ticketId) return
const [all, mine] = await Promise.all([fetchAllProjects(), fetchByTicketId(ticketId)])
setAssigned(mine)
setAvailable(assignableProjects(all, ticket, new Set(mine.map((p) => p.$id))))
}, [ticket, fetchAllProjects, fetchByTicketId])
setAvailable(assignableProjects(all, ticketRef.current, new Set(mine.map((p) => p.$id))))
}, [ticketId, fetchAllProjects, fetchByTicketId])
useEffect(() => {
let cancelled = false
@@ -156,16 +162,15 @@ export default function TicketProjects({ ticket }) {
}
return (
<div className="ui-card pad ticket-projects">
<h5 style={{ fontWeight: 700, marginBottom: 12 }}>Projekte ({assigned.length})</h5>
{importing && <p className="muted" style={{ marginTop: 0 }}>Suche neue Repos …</p>}
{importHint && <div className="text-red" style={{ marginBottom: 8 }}>{importHint}</div>}
{error && <div className="text-red" style={{ marginBottom: 8 }}>{error}</div>}
<Card title={`Projekte (${assigned.length})`}>
{importing && <p className="muted">Suche neue Repos …</p>}
{importHint && <p className="msg err" role="alert">{importHint}</p>}
{error && <p className="msg err" role="alert">{error}</p>}
{assigned.length === 0
? <p className="muted" style={{ marginTop: 0 }}>Noch kein Projekt an diesem Ticket.</p>
? <p className="muted">Noch kein Projekt an diesem Ticket.</p>
: assigned.map((p) => (
<div key={p.$id}>
<span className="muted" style={{ fontSize: 12 }} data-testid="project-kind">{kindLabelFor(kinds, p.kind)}</span>
<span className="muted xs" data-testid="project-kind">{kindLabelFor(kinds, p.kind)}</span>
<AssignedProjectCard
project={p}
showActions
@@ -177,7 +182,7 @@ export default function TicketProjects({ ticket }) {
))}
<div className="ticket-projects-assign">
<select className="form-control" aria-label="Projekt zuweisen" value={selected} onChange={(e) => setSelected(e.target.value)}>
<select className="select" aria-label="Projekt zuweisen" value={selected} onChange={(e) => setSelected(e.target.value)}>
<option value="">Projekt wählen …</option>
{available.map((p) => (
<option key={p.$id} value={p.$id}>
@@ -186,44 +191,43 @@ export default function TicketProjects({ ticket }) {
</option>
))}
</select>
<button type="button" className="ui-btn" disabled={!selected || busy} onClick={assign}><FaLink /> Zuweisen</button>
<button type="button" className="ui-btn ui-btn-ghost" onClick={() => setShowCreate((s) => !s)}><FaPlus /> Neues Projekt</button>
<Button type="button" disabled={!selected || busy} onClick={assign}>Zuweisen</Button>
<Button variant="quiet" type="button" onClick={() => setShowCreate((s) => !s)}>Neues Projekt</Button>
</div>
{assignResult && (
<div style={{ marginTop: 8, fontSize: 13 }}>
<div>{assignResult.text}</div>
<p className="msg">
{assignResult.text}
{assignResult.warnings.map((w, i) => (
<div key={i} className="muted" style={{ marginLeft: 12 }}>{w}</div>
<Fragment key={i}>
<br />
<span className="muted">{w}</span>
</Fragment>
))}
</div>
</p>
)}
{showCreate && (
<form className="ticket-projects-assign" onSubmit={create} style={{ marginTop: 8 }}>
<input className="form-control" placeholder="Anzeigename" value={createForm.displayName} onChange={(e) => setCreateForm((f) => ({ ...f, displayName: e.target.value }))} required />
<input className="form-control" placeholder="Subdomain" value={createForm.subdomain} onChange={(e) => setCreateForm((f) => ({ ...f, subdomain: e.target.value }))} required />
<button type="submit" className="ui-btn ui-btn-primary" disabled={busy}>{busy ? 'Wird angelegt …' : 'Anlegen & zuweisen'}</button>
<form className="ticket-projects-assign" onSubmit={create}>
<input className="input" aria-label="Anzeigename" placeholder="Anzeigename" value={createForm.displayName} onChange={(e) => setCreateForm((f) => ({ ...f, displayName: e.target.value }))} required />
<input className="input" aria-label="Subdomain" placeholder="Subdomain" value={createForm.subdomain} onChange={(e) => setCreateForm((f) => ({ ...f, subdomain: e.target.value }))} required />
<Button type="submit" variant="primary" disabled={busy}>{busy ? 'Wird angelegt …' : 'Anlegen & zuweisen'}</Button>
</form>
)}
{kindDialog && (
<div className="overlay">
<span className="overlay-close" onClick={() => setKindDialog(false)}>×</span>
<div className="overlay-content">
<h2 className="mb-2">Was ist das?</h2>
<p className="muted">Bitte wähle die Art des Projekts, bevor es zugewiesen wird.</p>
{kinds.length === 0 && (
<div className="text-red" style={{ marginBottom: 8 }}>Arten konnten nicht geladen werden – bitte neu laden.</div>
)}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{kinds.map((k) => (
<button key={k.key} type="button" className="ui-btn" onClick={() => assignWithKind(k.key)}>{k.label}</button>
))}
</div>
<Dialog title="Was ist das?" onClose={() => setKindDialog(false)}>
<p className="muted">Bitte wähle die Art des Projekts, bevor es zugewiesen wird.</p>
{kinds.length === 0 && (
<p className="msg err">Arten konnten nicht geladen werden – bitte neu laden.</p>
)}
<div className="row-buttons">
{kinds.map((k) => (
<Button key={k.key} type="button" onClick={() => assignWithKind(k.key)}>{k.label}</Button>
))}
</div>
</div>
</Dialog>
)}
</div>
</Card>
)
}