diff --git a/src/lib/projectList.js b/src/lib/projectList.js index 218a4db..53fac4f 100644 --- a/src/lib/projectList.js +++ b/src/lib/projectList.js @@ -26,3 +26,47 @@ export function projectsInView(projects, filter, { isAdmin = false } = {}) { if (filter === 'assigned') return act.filter((p) => Boolean(p.customerId)) return act } + +export const VIEW_LABELS = { all: 'Aktive Projekte', templates: 'Vorlagen', archived: 'Archiviert', everything: 'Alle inkl. Archiv' } + +// Ansichten, die ein Projekt zeigen koennen - die Vorlagen-Ansicht gibt es nur fuer Admins +const viewsFor = (isAdmin) => ['all', ...(isAdmin ? ['templates'] : []), 'archived', 'everything'] + +/** + * Ansicht, in der ein Projekt (z. B. nach dem Bearbeiten) zu sehen ist: die aktuelle, wenn es dort + * steht, sonst die erste passende. Ohne passende Ansicht bleibt es bei der aktuellen. + * Hintergrund: SHK-vorlage zur Vorlage gemacht -> verschwand kommentarlos aus "Aktive Projekte". + */ +export function viewShowing(project, filter, { isAdmin = false } = {}) { + if (!project) return filter + for (const view of [filter, ...viewsFor(isAdmin)]) { + if (view === 'templates' && !isAdmin) continue + if (projectsInView([project], view, { isAdmin }).length) return view + } + return filter +} + +/** Textsuche ueber Titel, Subdomain und Repo (leer passt immer). */ +export function matchesSearch(project, search) { + const q = String(search || '').trim().toLowerCase() + if (!q) return true + return [project.projectName, project.subdomain, project.repoFullName, project.giteaRepoName].some((v) => + String(v || '').toLowerCase().includes(q) + ) +} + +/** + * Treffer, die die aktuelle Ansicht ausblendet (Vorlagen nur fuer Admins, Archiv), je Ansicht gezaehlt. + * `matches` ist der Filter der Seite (Suche, Kunde, Mitarbeiter). + */ +export function hiddenMatches(projects, filter, matches, { isAdmin = false } = {}) { + if (filter === 'everything') return [] + const shown = new Set(projectsInView(projects, filter, { isAdmin }).map((p) => p.$id)) + const out = [] + for (const view of [...(isAdmin ? ['templates'] : []), 'archived']) { + if (view === filter) continue + const count = projectsInView(projects, view, { isAdmin }).filter((p) => !shown.has(p.$id) && matches(p)).length + if (count) out.push({ view, count }) + } + return out +} diff --git a/src/pages/ProjectsPage.jsx b/src/pages/ProjectsPage.jsx index 19acdd6..053f787 100644 --- a/src/pages/ProjectsPage.jsx +++ b/src/pages/ProjectsPage.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo } from 'react' +import { useState, useEffect, useMemo, useCallback } from 'react' import { useNavigate } from 'react-router-dom' import PreviewBlock from '../components/PreviewBlock' import { @@ -27,7 +27,7 @@ import { importNewReposThrottled, } from '../lib/projectAdminApi' import { assignSummary, ticketIdForCustomer } from '../lib/projectAssign' -import { projectCounts, projectsInView } from '../lib/projectList' +import { projectCounts, projectsInView, viewShowing, matchesSearch, hiddenMatches, VIEW_LABELS } from '../lib/projectList' import { slugify } from '../lib/subdomain' import PreviewLinkButton from '../components/PreviewLinkButton' import GiteaLinkButton from '../components/GiteaLinkButton' @@ -163,33 +163,28 @@ export default function ProjectsPage() { // 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 = projectsInView(projects, filter, { isAdmin }) - - // 2) Textsuche (Titel/Subdomain/Repo) - const q = search.trim().toLowerCase() - if (q) { - base = base.filter((p) => - [p.projectName, p.subdomain, p.repoFullName, p.giteaRepoName].some((v) => - String(v || '').toLowerCase().includes(q) - ) - ) - } - - // 3) Kunde - if (customerFilter) base = base.filter((p) => p.customerId === customerFilter) - - // 4) Mitarbeiter (ueber das zugeordnete Ticket -> assignedTo) + // Filter neben der Kategorie: Textsuche (Titel/Subdomain/Repo), Kunde, Mitarbeiter (ueber das + // zugeordnete Ticket -> assignedTo) + const pageMatches = useCallback((p) => { + if (!matchesSearch(p, search)) return false + if (customerFilter && p.customerId !== customerFilter) return false if (employeeFilter) { - base = base.filter((p) => { - const wo = p.ticketId ? workorderById[p.ticketId] : null - return wo && wo.assignedTo === employeeFilter - }) + const wo = p.ticketId ? workorderById[p.ticketId] : null + if (!wo || wo.assignedTo !== employeeFilter) return false } + return true + }, [search, customerFilter, employeeFilter, workorderById]) - return base - }, [projects, filter, isAdmin, search, customerFilter, employeeFilter, workorderById]) + const filteredProjects = useMemo( + () => projectsInView(projects, filter, { isAdmin }).filter(pageMatches), + [projects, filter, isAdmin, pageMatches] + ) + + // Suchtreffer in ausgeblendeten Ansichten (Vorlagen, Archiv) - sonst wirkt ein Projekt verschwunden + const hiddenHits = useMemo( + () => (search.trim() ? hiddenMatches(projects, filter, pageMatches, { isAdmin }) : []), + [projects, filter, isAdmin, search, pageMatches] + ) const handleCreate = async (e) => { e.preventDefault() @@ -314,14 +309,21 @@ export default function ProjectsPage() { if (editForm.projectType !== 'project') { payload.subdomain = slugify(editForm.subdomain) } - const result = await updateWebsiteProject(editProject.$id, payload) + const projectId = editProject.$id + const result = await updateWebsiteProject(projectId, payload) setEditProject(null) - setNotice( - result.previewUrl - ? `Projekt aktualisiert - ${result.isPublic ? 'oeffentlich' : 'Preview'}: ${result.previewUrl}` - : 'Projekt aktualisiert.' - ) - await fetchAllProjects() + let text = result.previewUrl + ? `Projekt aktualisiert - ${result.isPublic ? 'oeffentlich' : 'Preview'}: ${result.previewUrl}` + : 'Projekt aktualisiert.' + // Faellt das Projekt aus der aktuellen Ansicht (z. B. zur Vorlage gemacht), dorthin wechseln, + // wo es jetzt steht - sonst wirkt es verschwunden + const fresh = (await fetchAllProjects()).find((p) => p.$id === projectId) + const view = viewShowing(fresh, filter, { isAdmin }) + if (fresh && view !== filter) { + setFilter(view) + text += ` – „${fresh.projectName}“ steht jetzt unter „${VIEW_LABELS[view]}“.` + } + setNotice(text) } catch (err) { setEditError(err.message || 'Projekt konnte nicht aktualisiert werden') } finally { @@ -448,6 +450,24 @@ export default function ProjectsPage() { )} + {hiddenHits.length > 0 && ( +