Projektarten: Art beim Zuweisen und nach Repo-Anlage
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8653756935
commit
c16049d266
4 files changed
+85
-2
No files matched your search
@@ -4,6 +4,8 @@ import { useWebsiteProjects } from '../hooks/useWebsiteProjects'
|
|||||||
import { createProjectFromTemplate, importNewRepos } from '../lib/projectAdminApi'
|
import { createProjectFromTemplate, importNewRepos } from '../lib/projectAdminApi'
|
||||||
import { isOutsideOrg, assignSummary } from '../lib/projectAssign'
|
import { isOutsideOrg, assignSummary } from '../lib/projectAssign'
|
||||||
import AssignedProjectCard from './AssignedProjectCard'
|
import AssignedProjectCard from './AssignedProjectCard'
|
||||||
|
import { fetchServices, completeAssignment } from '../lib/projectKindsApi'
|
||||||
|
import { kindLabelFor } from '../lib/assignFlow'
|
||||||
|
|
||||||
function slugify(v) {
|
function slugify(v) {
|
||||||
return String(v || '').toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 100)
|
return String(v || '').toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 100)
|
||||||
@@ -35,6 +37,12 @@ export default function TicketProjects({ ticket }) {
|
|||||||
const [importing, setImporting] = useState(false)
|
const [importing, setImporting] = useState(false)
|
||||||
const [importHint, setImportHint] = useState('')
|
const [importHint, setImportHint] = useState('')
|
||||||
const [assignResult, setAssignResult] = useState(null)
|
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 () => {
|
const load = useCallback(async () => {
|
||||||
if (!ticket?.$id) return
|
if (!ticket?.$id) return
|
||||||
@@ -73,7 +81,17 @@ export default function TicketProjects({ ticket }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const assign = async () => {
|
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)
|
setBusy(true)
|
||||||
setError('')
|
setError('')
|
||||||
setAssignResult(null)
|
setAssignResult(null)
|
||||||
@@ -91,6 +109,35 @@ export default function TicketProjects({ ticket }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
setBusy(true)
|
||||||
|
setError('')
|
||||||
|
setAssignResult(null)
|
||||||
|
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 unassign = (projectId) => run(async () => {
|
||||||
const r = await unassignProject(projectId)
|
const r = await unassignProject(projectId)
|
||||||
if (!r.success) throw new Error(r.error)
|
if (!r.success) throw new Error(r.error)
|
||||||
@@ -121,14 +168,16 @@ export default function TicketProjects({ ticket }) {
|
|||||||
{assigned.length === 0
|
{assigned.length === 0
|
||||||
? <p className="muted" style={{ marginTop: 0 }}>Noch kein Projekt an diesem Ticket.</p>
|
? <p className="muted" style={{ marginTop: 0 }}>Noch kein Projekt an diesem Ticket.</p>
|
||||||
: assigned.map((p) => (
|
: assigned.map((p) => (
|
||||||
|
<div key={p.$id}>
|
||||||
|
<span className="muted" style={{ fontSize: 12 }} data-testid="project-kind">{kindLabelFor(kinds, p.kind)}</span>
|
||||||
<AssignedProjectCard
|
<AssignedProjectCard
|
||||||
key={p.$id}
|
|
||||||
project={p}
|
project={p}
|
||||||
showActions
|
showActions
|
||||||
onUnassign={unassign}
|
onUnassign={unassign}
|
||||||
customerName={ticket.customerName || ''}
|
customerName={ticket.customerName || ''}
|
||||||
onChanged={load}
|
onChanged={load}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="ticket-projects-assign">
|
<div className="ticket-projects-assign">
|
||||||
@@ -161,6 +210,21 @@ export default function TicketProjects({ ticket }) {
|
|||||||
<button type="submit" className="ui-btn ui-btn-primary" disabled={busy}>{busy ? 'Wird angelegt …' : 'Anlegen & zuweisen'}</button>
|
<button type="submit" className="ui-btn ui-btn-primary" disabled={busy}>{busy ? 'Wird angelegt …' : 'Anlegen & zuweisen'}</button>
|
||||||
</form>
|
</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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -55,3 +55,9 @@ export function openTicketsFor(tickets, customerId) {
|
|||||||
(t) => t.customerId === customerId && t.status !== 'Closed' && t.status !== 'Cancelled'
|
(t) => t.customerId === customerId && t.status !== 'Closed' && t.status !== 'Cancelled'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Art-Label fuer die Anzeige; leere Art = "Art fehlt"
|
||||||
|
export function kindLabelFor(kinds, key) {
|
||||||
|
if (!key) return 'Art fehlt'
|
||||||
|
return (kinds || []).find((k) => k.key === key)?.label || key
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react'
|
import { useState, useEffect, useMemo } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
FaFolder,
|
FaFolder,
|
||||||
FaPlus,
|
FaPlus,
|
||||||
@@ -88,6 +89,7 @@ function isProjectReady(project) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ProjectsPage() {
|
export default function ProjectsPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
const { projects, loading, error, fetchAllProjects, assignProjects, unassignProject } = useWebsiteProjects()
|
const { projects, loading, error, fetchAllProjects, assignProjects, unassignProject } = useWebsiteProjects()
|
||||||
const { customers } = useCustomers()
|
const { customers } = useCustomers()
|
||||||
const { workorders } = useWorkorders(WORKORDER_FILTERS)
|
const { workorders } = useWorkorders(WORKORDER_FILTERS)
|
||||||
@@ -223,6 +225,8 @@ export default function ProjectsPage() {
|
|||||||
: 'Projekt (ohne Website) angelegt.'
|
: 'Projekt (ohne Website) angelegt.'
|
||||||
)
|
)
|
||||||
await fetchAllProjects()
|
await fetchAllProjects()
|
||||||
|
// Direkt zur Art-/Kunden-Zuordnung des neuen Repos
|
||||||
|
if (result.projectId) navigate(`/projects/zuordnen/${result.projectId}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setCreateError(err.message || 'Projekt konnte nicht angelegt werden')
|
setCreateError(err.message || 'Projekt konnte nicht angelegt werden')
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -70,3 +70,12 @@ test('openTicketsFor', () => {
|
|||||||
assert.deepEqual(openTicketsFor(t, 'c1').map((x) => x.$id), ['1', '5'])
|
assert.deepEqual(openTicketsFor(t, 'c1').map((x) => x.$id), ['1', '5'])
|
||||||
assert.deepEqual(openTicketsFor(null, 'c1'), [])
|
assert.deepEqual(openTicketsFor(null, 'c1'), [])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('kindLabelFor: Label aus Katalog, leer = Art fehlt', async () => {
|
||||||
|
const { kindLabelFor } = await loadModule('src/lib/assignFlow.js')
|
||||||
|
const kinds = [{ key: 'website', label: 'Website' }, { key: 'app', label: 'App' }]
|
||||||
|
assert.equal(kindLabelFor(kinds, 'app'), 'App')
|
||||||
|
assert.equal(kindLabelFor(kinds, ''), 'Art fehlt')
|
||||||
|
assert.equal(kindLabelFor(kinds, undefined), 'Art fehlt')
|
||||||
|
assert.equal(kindLabelFor(kinds, 'unbekannt'), 'unbekannt')
|
||||||
|
})
|
||||||
Reference in new issue
Block a user