Stelle Website-Projekte und Admin-Tabs nach Auto-Deploy-Reset wieder her.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,37 +1,229 @@
|
||||
import { useState } from 'react'
|
||||
import { FaFolder, FaPlus } from 'react-icons/fa6'
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { FaFolder, FaPlus, FaArrowUpRightFromSquare } from 'react-icons/fa6'
|
||||
import { useWebsiteProjects } from '../hooks/useWebsiteProjects'
|
||||
import { useCustomers } from '../hooks/useCustomers'
|
||||
import { useWorkorders } from '../hooks/useWorkorders'
|
||||
import { createProjectFromTemplate } from '../lib/projectAdminApi'
|
||||
|
||||
function slugify(value) {
|
||||
return String(value || '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 100)
|
||||
}
|
||||
|
||||
const WORKORDER_FILTERS = { limit: 500 }
|
||||
|
||||
export default function ProjectsPage() {
|
||||
const [projects] = useState([])
|
||||
const { projects, loading, error, fetchAllProjects } = useWebsiteProjects()
|
||||
const { customers } = useCustomers()
|
||||
const { workorders } = useWorkorders(WORKORDER_FILTERS)
|
||||
const [filter, setFilter] = useState('all')
|
||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||
const [createForm, setCreateForm] = useState({
|
||||
displayName: '',
|
||||
subdomain: '',
|
||||
repoName: '',
|
||||
customerId: '',
|
||||
})
|
||||
const [createLoading, setCreateLoading] = useState(false)
|
||||
const [createError, setCreateError] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
fetchAllProjects()
|
||||
}, [fetchAllProjects])
|
||||
|
||||
const customerMap = useMemo(() => {
|
||||
const map = {}
|
||||
for (const c of customers) {
|
||||
map[c.$id] = c.name || c.code || c.$id
|
||||
}
|
||||
return map
|
||||
}, [customers])
|
||||
|
||||
const ticketMap = useMemo(() => {
|
||||
const map = {}
|
||||
for (const wo of workorders) {
|
||||
map[wo.$id] = wo.woid || wo.$id
|
||||
}
|
||||
return map
|
||||
}, [workorders])
|
||||
|
||||
const filteredProjects = useMemo(() => {
|
||||
if (filter === 'unassigned') {
|
||||
return projects.filter((p) => !p.customerId)
|
||||
}
|
||||
if (filter === 'assigned') {
|
||||
return projects.filter((p) => Boolean(p.customerId))
|
||||
}
|
||||
return projects
|
||||
}, [projects, filter])
|
||||
|
||||
const handleCreate = async (e) => {
|
||||
e.preventDefault()
|
||||
setCreateLoading(true)
|
||||
setCreateError('')
|
||||
try {
|
||||
await createProjectFromTemplate({
|
||||
repoName: createForm.repoName || createForm.subdomain,
|
||||
displayName: createForm.displayName,
|
||||
subdomain: slugify(createForm.subdomain),
|
||||
customerId: createForm.customerId || '',
|
||||
ticketId: '',
|
||||
})
|
||||
setShowCreateModal(false)
|
||||
setCreateForm({ displayName: '', subdomain: '', repoName: '', customerId: '' })
|
||||
await fetchAllProjects()
|
||||
} catch (err) {
|
||||
setCreateError(err.message || 'Projekt konnte nicht angelegt werden')
|
||||
} finally {
|
||||
setCreateLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="main-content">
|
||||
<header className="text-center mb-2">
|
||||
<h2>Projects</h2>
|
||||
<h2>Website-Projekte</h2>
|
||||
</header>
|
||||
|
||||
<div className="text-center mb-2">
|
||||
<button className="btn btn-teal">
|
||||
<FaPlus /> New Project
|
||||
<div className="text-center mb-2" style={{ display: 'flex', gap: '12px', justifyContent: 'center', flexWrap: 'wrap' }}>
|
||||
<select
|
||||
className="form-control"
|
||||
style={{ width: 'auto', minWidth: '180px' }}
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
>
|
||||
<option value="all">Alle Projekte</option>
|
||||
<option value="unassigned">Unzugeordnet</option>
|
||||
<option value="assigned">Zugeordnet</option>
|
||||
</select>
|
||||
<button type="button" className="btn btn-teal" onClick={() => setShowCreateModal(true)}>
|
||||
<FaPlus /> Neues Projekt
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{projects.length === 0 ? (
|
||||
{error && (
|
||||
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<p className="text-center text-grey">Projekte werden geladen...</p>
|
||||
) : filteredProjects.length === 0 ? (
|
||||
<div className="text-center p-4">
|
||||
<FaFolder size={64} className="text-grey" />
|
||||
<p className="text-grey mt-2">No projects yet. Create your first project to get started.</p>
|
||||
<p className="text-grey mt-2">Keine Projekte gefunden.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="projects-grid">
|
||||
{projects.map(project => (
|
||||
<div key={project.$id} className="project-card">
|
||||
<h4>{project.name}</h4>
|
||||
<p className="text-grey">{project.description}</p>
|
||||
<div className="project-meta">
|
||||
<span>{project.ticketCount || 0} tickets</span>
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table className="table" style={{ width: '100%', fontSize: '14px' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Subdomain</th>
|
||||
<th>Template</th>
|
||||
<th>Kunde</th>
|
||||
<th>Ticket (WOID)</th>
|
||||
<th>Status</th>
|
||||
<th>Preview</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredProjects.map((project) => (
|
||||
<tr key={project.$id}>
|
||||
<td>{project.projectName}</td>
|
||||
<td>{project.subdomain}</td>
|
||||
<td>{project.templateName || '-'}</td>
|
||||
<td>{project.customerId ? customerMap[project.customerId] || project.customerId : '-'}</td>
|
||||
<td>{project.ticketId ? ticketMap[project.ticketId] || project.ticketId : '-'}</td>
|
||||
<td>{project.status || '-'}</td>
|
||||
<td>
|
||||
{project.previewUrl ? (
|
||||
<a href={project.previewUrl} target="_blank" rel="noopener noreferrer">
|
||||
<FaArrowUpRightFromSquare /> Link
|
||||
</a>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showCreateModal && (
|
||||
<div className="overlay">
|
||||
<span className="overlay-close" onClick={() => setShowCreateModal(false)}>×</span>
|
||||
<div className="overlay-content">
|
||||
<h2 className="mb-2">Neues Website-Projekt</h2>
|
||||
{createError && (
|
||||
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
|
||||
{createError}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
<form onSubmit={handleCreate}>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Anzeigename</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={createForm.displayName}
|
||||
onChange={(e) => setCreateForm((p) => ({ ...p, displayName: e.target.value }))}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Subdomain</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={createForm.subdomain}
|
||||
onChange={(e) =>
|
||||
setCreateForm((p) => ({
|
||||
...p,
|
||||
subdomain: e.target.value,
|
||||
repoName: p.repoName || slugify(e.target.value),
|
||||
}))
|
||||
}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Repo-Name (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={createForm.repoName}
|
||||
onChange={(e) => setCreateForm((p) => ({ ...p, repoName: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Kunde (optional)</label>
|
||||
<select
|
||||
className="form-control"
|
||||
value={createForm.customerId}
|
||||
onChange={(e) => setCreateForm((p) => ({ ...p, customerId: e.target.value }))}
|
||||
>
|
||||
<option value="">Kein Kunde</option>
|
||||
{customers.map((c) => (
|
||||
<option key={c.$id} value={c.$id}>
|
||||
({c.code || ''}) {c.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="text-center mt-2">
|
||||
<button type="submit" className="btn btn-dark" disabled={createLoading}>
|
||||
{createLoading ? 'Wird angelegt...' : 'Projekt anlegen'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user