Stelle Website-Projekte und Admin-Tabs nach Auto-Deploy-Reset wieder her.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
108
src/components/WebpageProjectPanel.jsx
Normal file
108
src/components/WebpageProjectPanel.jsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { FaExternalLinkAlt, FaPlus, FaTrash } from 'react-icons/fa'
|
||||
import { useWebsiteProjects } from '../hooks/useWebsiteProjects'
|
||||
import { createProjectFromTemplate } from '../lib/projectAdminApi'
|
||||
import { WEBPAGE_TICKET_TYPE } from '../lib/appwrite'
|
||||
|
||||
function slugify(v) {
|
||||
return String(v || '').toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 100)
|
||||
}
|
||||
|
||||
export default function WebpageProjectPanel({ ticket }) {
|
||||
const { fetchAllProjects, fetchByTicketId, getAvailableProjects, assignProjects, unassignProject } = useWebsiteProjects()
|
||||
const [assigned, setAssigned] = useState([])
|
||||
const [available, setAvailable] = useState([])
|
||||
const [selectedIds, setSelectedIds] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [actionLoading, setActionLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [createForm, setCreateForm] = useState({ displayName: '', subdomain: '', repoName: '' })
|
||||
|
||||
const loadProjects = useCallback(async () => {
|
||||
if (!ticket?.$id || ticket.type !== WEBPAGE_TICKET_TYPE) return
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
const [all, mine] = await Promise.all([fetchAllProjects(), fetchByTicketId(ticket.$id)])
|
||||
setAssigned(mine)
|
||||
const ids = new Set(mine.map((p) => p.$id))
|
||||
setAvailable(getAvailableProjects(all, ticket.customerId).filter((p) => !ids.has(p.$id)))
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [ticket, fetchAllProjects, fetchByTicketId, getAvailableProjects])
|
||||
|
||||
useEffect(() => { loadProjects() }, [loadProjects])
|
||||
if (ticket?.type !== WEBPAGE_TICKET_TYPE) return null
|
||||
|
||||
const handleAssign = async () => {
|
||||
if (!selectedIds.length || !ticket.customerId) return
|
||||
setActionLoading(true)
|
||||
const r = await assignProjects(selectedIds, { customerId: ticket.customerId, ticketId: ticket.$id })
|
||||
setActionLoading(false)
|
||||
if (r.success) { setSelectedIds([]); await loadProjects() } else setError(r.error)
|
||||
}
|
||||
|
||||
const handleCreate = async (e) => {
|
||||
e.preventDefault()
|
||||
if (!ticket.customerId) { setError('Kein Kunde am Ticket.'); return }
|
||||
setActionLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
await createProjectFromTemplate({
|
||||
repoName: createForm.repoName || createForm.subdomain,
|
||||
displayName: createForm.displayName,
|
||||
subdomain: slugify(createForm.subdomain),
|
||||
customerId: ticket.customerId,
|
||||
ticketId: ticket.$id,
|
||||
})
|
||||
setCreateForm({ displayName: '', subdomain: '', repoName: '' })
|
||||
await loadProjects()
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setActionLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ background: 'rgba(45,55,72,0.5)', borderRadius: 12, padding: 20, border: '1px solid rgba(59,130,246,0.3)', marginTop: 20 }}>
|
||||
<h5 style={{ fontWeight: 'bold', marginBottom: 12 }}>Website-Projekte</h5>
|
||||
{error && <div className="bg-red text-white p-2 mb-2">{error}</div>}
|
||||
{loading ? <p className="text-grey">Laden...</p> : (
|
||||
<>
|
||||
<section style={{ marginBottom: 16 }}>
|
||||
<h6>Zugewiesen</h6>
|
||||
{assigned.length === 0 ? <p className="text-grey">Keine Projekte.</p> : assigned.map((p) => (
|
||||
<div key={p.$id} style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 8 }}>
|
||||
<span>{p.projectName} ({p.subdomain})</span>
|
||||
{p.previewUrl && <a href={p.previewUrl} target="_blank" rel="noreferrer"><FaExternalLinkAlt /></a>}
|
||||
<button type="button" className="btn btn-sm" onClick={() => unassignProject(p.$id).then(loadProjects)}><FaTrash /></button>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
<section style={{ marginBottom: 16 }}>
|
||||
<h6>Verf<EFBFBD>gbar zuweisen</h6>
|
||||
{available.map((p) => (
|
||||
<label key={p.$id} style={{ display: 'block' }}>
|
||||
<input type="checkbox" checked={selectedIds.includes(p.$id)} onChange={() => setSelectedIds((s) => s.includes(p.$id) ? s.filter((x) => x !== p.$id) : [...s, p.$id])} />
|
||||
{' '}{p.projectName} ({p.subdomain})
|
||||
</label>
|
||||
))}
|
||||
<button type="button" className="btn btn-teal mt-2" disabled={!selectedIds.length || actionLoading} onClick={handleAssign}>Zuweisen</button>
|
||||
</section>
|
||||
<section>
|
||||
<h6><FaPlus /> Neues Projekt</h6>
|
||||
<form onSubmit={handleCreate} style={{ display: 'grid', gap: 8, maxWidth: 400 }}>
|
||||
<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, repoName: f.repoName || slugify(e.target.value) }))} required />
|
||||
<button type="submit" className="btn btn-dark" disabled={actionLoading}>Anlegen & zuweisen</button>
|
||||
</form>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user