Files
tickte-system/src/pages/ProjectsPage.jsx
2026-05-25 06:41:35 +00:00

232 lines
7.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, 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>Website-Projekte</h2>
</header>
<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>
{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">Keine Projekte gefunden.</p>
</div>
) : (
<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>
)}
<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>
)
}