Projekte im Ticketsystem: Anlegen, Archiv und Anzeige repariert
- Neues Projekt (Projekte-Seite): Repo-Name blieb nach dem ersten
Buchstaben der Subdomain stehen ("m" statt "musterfirma")
- Ticket "Projekt waehlen": archivierte Repos nach Flag ausblenden, nicht
nach status (reaktivierte fehlten, archivierte wurden angeboten)
- Projektliste: "Aktive Projekte" + neue Ansicht "Alle inkl. Archiv";
Liste wird auch nach einem Anlege-Fehler neu geladen
- Kundenauswahl: "Kein Kunde" entfernt Kunde und Ticket wirklich (mit
Rueckfrage); beim Kundenwechsel bleibt kein fremdes Ticket haengen
- Zuordnen-Assistent ueberschreibt eine vorhandene Website nicht mehr
- Neues Projekt am Ticket bekommt die Art Website
- Umlaute in Subdomains wie im Portal (mueller statt m-ller)
- Preview-Block nur mit gespeicherter Subdomain; archivierte Projekte
erst reaktivieren, bevor sie bearbeitet werden
- Repo-Import beim Aufklappen hoechstens einmal pro Minute
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
78bc99a73b
commit
f4c8b235b9
13 files changed
+269
-92
No files matched your search
+34
-46
@@ -25,18 +25,12 @@ import {
|
||||
archiveWebsiteProject,
|
||||
unarchiveWebsiteProject,
|
||||
} from '../lib/projectAdminApi'
|
||||
import { assignSummary } from '../lib/projectAssign'
|
||||
import { assignSummary, ticketIdForCustomer } from '../lib/projectAssign'
|
||||
import { projectCounts, projectsInView } from '../lib/projectList'
|
||||
import { slugify } from '../lib/subdomain'
|
||||
import PreviewLinkButton from '../components/PreviewLinkButton'
|
||||
import GiteaLinkButton from '../components/GiteaLinkButton'
|
||||
|
||||
function slugify(value) {
|
||||
return String(value || '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 100)
|
||||
}
|
||||
|
||||
const WORKORDER_FILTERS = { limit: 500 }
|
||||
|
||||
// Projekt-Typen (= "Template"/Hosting-Modus)
|
||||
@@ -91,7 +85,7 @@ function isProjectReady(project) {
|
||||
|
||||
export default function ProjectsPage() {
|
||||
const navigate = useNavigate()
|
||||
const { projects, loading, error, fetchAllProjects, assignProjects, unassignProject } = useWebsiteProjects()
|
||||
const { projects, loading, error, fetchAllProjects, assignProjects, clearCustomer } = useWebsiteProjects()
|
||||
const { customers } = useCustomers()
|
||||
const { workorders } = useWorkorders(WORKORDER_FILTERS)
|
||||
const { employees } = useEmployees()
|
||||
@@ -158,29 +152,12 @@ export default function ProjectsPage() {
|
||||
[employees]
|
||||
)
|
||||
|
||||
const counts = useMemo(() => {
|
||||
// Vorlagen sind aus den normalen Ansichten ausgeblendet (eigene Ansicht, nur Admin)
|
||||
const active = projects.filter((p) => !p.archived && !p.isTemplate)
|
||||
return {
|
||||
all: active.length,
|
||||
unassigned: active.filter((p) => !p.customerId).length,
|
||||
assigned: active.filter((p) => Boolean(p.customerId)).length,
|
||||
archived: projects.filter((p) => p.archived).length,
|
||||
templates: projects.filter((p) => p.isTemplate && !p.archived).length,
|
||||
}
|
||||
}, [projects])
|
||||
// Vorlagen sind aus den normalen Ansichten ausgeblendet (eigene Ansicht, nur Admin)
|
||||
const counts = useMemo(() => projectCounts(projects, { isAdmin }), [projects, isAdmin])
|
||||
|
||||
const filteredProjects = useMemo(() => {
|
||||
// 1) Kategorie (Dropdown)
|
||||
let base
|
||||
if (filter === 'templates') base = projects.filter((p) => p.isTemplate && !p.archived)
|
||||
else if (filter === 'archived') base = projects.filter((p) => p.archived)
|
||||
else {
|
||||
const active = projects.filter((p) => !p.archived && !p.isTemplate)
|
||||
if (filter === 'unassigned') base = active.filter((p) => !p.customerId)
|
||||
else if (filter === 'assigned') base = active.filter((p) => Boolean(p.customerId))
|
||||
else base = active
|
||||
}
|
||||
let base = projectsInView(projects, filter, { isAdmin })
|
||||
|
||||
// 2) Textsuche (Titel/Subdomain/Repo)
|
||||
const q = search.trim().toLowerCase()
|
||||
@@ -204,7 +181,7 @@ export default function ProjectsPage() {
|
||||
}
|
||||
|
||||
return base
|
||||
}, [projects, filter, search, customerFilter, employeeFilter, workorderById])
|
||||
}, [projects, filter, isAdmin, search, customerFilter, employeeFilter, workorderById])
|
||||
|
||||
const handleCreate = async (e) => {
|
||||
e.preventDefault()
|
||||
@@ -212,7 +189,7 @@ export default function ProjectsPage() {
|
||||
setCreateError('')
|
||||
try {
|
||||
const result = await createProjectFromTemplate({
|
||||
repoName: createForm.repoName || slugify(createForm.subdomain) || slugify(createForm.displayName),
|
||||
repoName: slugify(createForm.repoName) || slugify(createForm.subdomain) || slugify(createForm.displayName),
|
||||
displayName: createForm.displayName,
|
||||
subdomain: slugify(createForm.subdomain),
|
||||
customerId: createForm.customerId || '',
|
||||
@@ -230,6 +207,8 @@ export default function ProjectsPage() {
|
||||
if (result.projectId) navigate(`/projects/zuordnen/${result.projectId}`)
|
||||
} catch (err) {
|
||||
setCreateError(err.message || 'Projekt konnte nicht angelegt werden')
|
||||
// Das Repo kann trotz Fehler entstanden sein - Liste trotzdem neu laden
|
||||
fetchAllProjects()
|
||||
} finally {
|
||||
setCreateLoading(false)
|
||||
}
|
||||
@@ -253,13 +232,14 @@ export default function ProjectsPage() {
|
||||
setAssignBusyId(project.$id)
|
||||
setNotice('')
|
||||
if (customerId) {
|
||||
const r = await assignProjects([project.$id], { customerId, ticketId: project.ticketId || '' })
|
||||
const ticketId = ticketIdForCustomer(project, customerId, workorderById)
|
||||
const r = await assignProjects([project.$id], { customerId, ticketId })
|
||||
const one = r.results[0]
|
||||
const summary = assignSummary(project.projectName, one.ok ? one.result : { error: one.error })
|
||||
setNotice(summary.warnings.length ? `${summary.text} – ${summary.warnings.join(' – ')}` : summary.text)
|
||||
} else {
|
||||
const r = await unassignProject(project.$id)
|
||||
if (!r.success) setNotice('Zuordnung fehlgeschlagen: ' + (r.error || 'unbekannt'))
|
||||
} else if (window.confirm(`Kunde von "${project.projectName}" entfernen? Der Kunde sieht das Projekt dann nicht mehr im Portal, das Ticket wird gelöst.`)) {
|
||||
const r = await clearCustomer(project.$id)
|
||||
setNotice(r.success ? `"${project.projectName}" hat keinen Kunden mehr.` : 'Zuordnung fehlgeschlagen: ' + (r.error || 'unbekannt'))
|
||||
}
|
||||
await fetchAllProjects()
|
||||
setAssignBusyId('')
|
||||
@@ -366,12 +346,13 @@ export default function ProjectsPage() {
|
||||
}
|
||||
|
||||
const FILTER_OPTIONS = [
|
||||
{ value: 'all', label: `Alle Projekte (${counts.all})` },
|
||||
{ value: 'all', label: `Aktive Projekte (${counts.all})` },
|
||||
{ value: 'unassigned', label: `Unzugeordnet (${counts.unassigned})` },
|
||||
{ value: 'assigned', label: `Zugeordnet (${counts.assigned})` },
|
||||
// Vorlagen-Ansicht nur fuer Admins
|
||||
...(isAdmin ? [{ value: 'templates', label: `Vorlagen (${counts.templates})` }] : []),
|
||||
{ value: 'archived', label: `Archiviert (${counts.archived})` },
|
||||
{ value: 'everything', label: `Alle inkl. Archiv (${counts.everything})` },
|
||||
]
|
||||
|
||||
// Typ-Optionen im Bearbeiten-Modal: adminOnly-Typen (Vorlage) nur fuer Admins
|
||||
@@ -627,13 +608,9 @@ export default function ProjectsPage() {
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={createForm.subdomain}
|
||||
onChange={(e) =>
|
||||
setCreateForm((p) => ({
|
||||
...p,
|
||||
subdomain: e.target.value,
|
||||
repoName: p.repoName || slugify(e.target.value),
|
||||
}))
|
||||
}
|
||||
// Repo-Name NICHT hier mitfuellen: nach dem ersten Buchstaben blieb er sonst
|
||||
// stehen (Repo "m" statt "musterfirma"). Leer = beim Anlegen aus Subdomain/Name.
|
||||
onChange={(e) => setCreateForm((p) => ({ ...p, subdomain: e.target.value }))}
|
||||
placeholder="leer lassen = Projekt ohne Website"
|
||||
/>
|
||||
<small className="text-grey">
|
||||
@@ -747,12 +724,23 @@ export default function ProjectsPage() {
|
||||
? <><FaBoxOpen /> Reaktivieren</>
|
||||
: <><FaBoxArchive /> Repository archivieren</>}
|
||||
</button>
|
||||
<button type="submit" className="btn btn-dark" disabled={editLoading || archiveBusy}>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-dark"
|
||||
disabled={editLoading || archiveBusy || editProject.archived}
|
||||
title={editProject.archived ? 'Archivierte Projekte erst reaktivieren' : undefined}
|
||||
>
|
||||
{editLoading ? 'Speichert...' : 'Speichern & synchronisieren'}
|
||||
</button>
|
||||
</div>
|
||||
{editProject.archived && (
|
||||
<small className="text-grey" style={{ display: 'block', marginTop: 8 }}>
|
||||
Archiviert: zum Bearbeiten oder Hosten erst reaktivieren.
|
||||
</small>
|
||||
)}
|
||||
</form>
|
||||
{editForm.projectType !== 'project' && (
|
||||
{/* Nur mit gespeicherter Subdomain - sonst meldet der Block "keine Preview-Subdomain" */}
|
||||
{editForm.projectType !== 'project' && editProject.subdomain && !editProject.archived && (
|
||||
<div className="form-group" style={{ marginTop: 16 }}>
|
||||
<label className="form-label">Preview</label>
|
||||
<PreviewBlock key={editProject.$id} projectId={editProject.$id} />
|
||||
|
||||
Reference in new issue
Block a user