diff --git a/README.md b/README.md index 5599a96..147fab4 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Datum speichert WOMS als Text `dd.mm.yyyy`, Uhrzeiten als `hhmm` (z. B. `0930`). | `employees` | Mitarbeiter | `userId` (Appwrite-Benutzer), `displayName`, `email`, `shortcode` (Kürzel) | | `websiteProjects` | Website-Projekte und Vorschauen | `customerId` (steuert, was der Kunde im Portal sieht), `ticketId` (Ziel der Git-Push-Arbeitsblätter), Subdomain, Repo | | `leads`, `leadPipeline`, `leadSettings` | Lead-Automatik | Recherche-Ergebnisse, Pipeline-Schritte, Sucheinstellungen | -| `roadmaps`, `roadmapNodes` | Projektpläne | Plan mit Schritten, verknüpft mit einem Ticket | +| `roadmaps`, `roadmapNodes` | alte Projektpläne | wird von WOMS nicht mehr gelesen (Plan mit Schritten, verknüpft mit einem Ticket) | | `config` | alte Einstellungen | wird von WOMS nicht mehr gelesen (Prioritäten, alte Listen) | Zusammenspiel mit anderen Systemen: diff --git a/src/App.jsx b/src/App.jsx index 4b93d00..6f61cd7 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -6,8 +6,6 @@ import LoginPage from './pages/LoginPage' import TicketsPage from './pages/TicketsPage' import PlanboardPage from './pages/PlanboardPage' import ProjectsPage from './pages/ProjectsPage' -import RoadmapsPage from './pages/RoadmapsPage' -import RoadmapDetailPage from './pages/RoadmapDetailPage' import AdminPage from './pages/AdminPage' import CustomersPage from './pages/CustomersPage' import LeadsPage from './pages/LeadsPage' @@ -47,10 +45,8 @@ function AppRoutes() { } /> } /> } /> - } /> - } /> } /> - {/* Alte Links (z. B. die entfernten Seiten Assets, Reports, Docs) landen bei den Tickets */} + {/* Alte Links (z. B. die entfernten Seiten Assets, Reports, Docs, Roadmaps) landen bei den Tickets */} } /> ) diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index aa9df30..fff4ddb 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -8,7 +8,6 @@ import { IconReportMoney, IconFolders, IconCalendar, - IconRoute, IconSettings, IconLogout, IconLayoutSidebarLeftCollapse, @@ -26,11 +25,10 @@ export default function Sidebar() { navigate('/login') } - // Eine flache Liste: fuer ein kleines Team reichen acht Eintraege ohne Gruppen + // Eine flache Liste: fuer ein kleines Team reichen sieben Eintraege ohne Gruppen const links = [ { label: 'Tickets', href: '/tickets', icon: }, { label: 'Planboard', href: '/planboard', icon: }, - { label: 'Roadmaps', href: '/roadmaps', icon: }, { label: 'Projekte', href: '/projects', icon: }, { label: 'Kunden', href: '/customers', icon: }, { label: 'Leads', href: '/leads', icon: }, diff --git a/src/components/ProjectContextPanel.jsx b/src/components/ProjectContextPanel.jsx index 878b46c..ce44b14 100644 --- a/src/components/ProjectContextPanel.jsx +++ b/src/components/ProjectContextPanel.jsx @@ -3,13 +3,12 @@ import { Link } from 'react-router-dom' import { IconFolders } from '@tabler/icons-react' import { StatusPill } from './ui/StatusPill' import { databases, DATABASE_ID, COLLECTIONS, Query } from '../lib/appwrite' -import { fetchRoadmapByTicketId } from '../lib/roadmaps' import { useWebsiteProjects } from '../hooks/useWebsiteProjects' /** - * Projekt-Kontextbox im Ticket: zeigt automatisch das zugehoerige Projekt, - * alle anderen Tickets desselben Kunden (klickbar) und den Link zum aktiven Plan. - * Rendert nichts, wenn das Ticket keinen Projektbezug hat. + * Projekt-Kontextbox im Ticket: zeigt automatisch das zugewiesene Projekt + * und alle anderen Tickets desselben Kunden (klickbar). + * Rendert nichts, wenn dem Ticket kein Projekt zugewiesen ist. */ export default function ProjectContextPanel({ ticket }) { const { fetchByTicketId } = useWebsiteProjects() @@ -19,16 +18,12 @@ export default function ProjectContextPanel({ ticket }) { let cancelled = false async function load() { try { - // Projektbezug: Roadmap dieses Tickets oder direkt zugewiesenes Projekt - const [roadmap, assignedProjects] = await Promise.all([ - fetchRoadmapByTicketId(ticket.$id).catch(() => null), - fetchByTicketId(ticket.$id), - ]) - const projectName = roadmap?.projectName || assignedProjects[0]?.projectName || '' - if (!roadmap && !assignedProjects.length) { + const assignedProjects = await fetchByTicketId(ticket.$id) + if (!assignedProjects.length) { if (!cancelled) setContext(null) return } + const projectName = assignedProjects[0].projectName || '' let otherTickets = [] if (ticket.customerId) { @@ -39,7 +34,7 @@ export default function ProjectContextPanel({ ticket }) { ]) otherTickets = res.documents.filter((t) => t.$id !== ticket.$id) } - if (!cancelled) setContext({ roadmap, projectName, otherTickets }) + if (!cancelled) setContext({ projectName, otherTickets }) } catch { if (!cancelled) setContext(null) } @@ -49,7 +44,7 @@ export default function ProjectContextPanel({ ticket }) { }, [ticket.$id, ticket.customerId, fetchByTicketId]) if (!context) return null - const { roadmap, projectName, otherTickets } = context + const { projectName, otherTickets } = context return (
@@ -57,11 +52,6 @@ export default function ProjectContextPanel({ ticket }) {
Projekt: {projectName || '-'}
- {roadmap && ( - - Plan: {roadmap.title} - - )}
{!ticket.customerId ? ( diff --git a/src/components/TicketCard.jsx b/src/components/TicketCard.jsx index 4cb0aeb..c7f2714 100644 --- a/src/components/TicketCard.jsx +++ b/src/components/TicketCard.jsx @@ -10,7 +10,6 @@ import WorksheetList from './WorksheetList' import WorksheetTimesModal from './WorksheetTimesModal' import WorksheetSummary from './WorksheetSummary' import TicketProjects from './TicketProjects' -import TicketRoadmapPanel from './TicketRoadmapPanel' import ProjectContextPanel from './ProjectContextPanel' import InvoicePanel from './InvoicePanel' import Tabs from './ui/Tabs' @@ -121,7 +120,6 @@ export default function TicketCard({ ticket, onUpdate }) {

- )} diff --git a/src/components/TicketRoadmapPanel.jsx b/src/components/TicketRoadmapPanel.jsx deleted file mode 100644 index 58760b5..0000000 --- a/src/components/TicketRoadmapPanel.jsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useEffect, useState } from 'react' -import { Link } from 'react-router-dom' -import { IconRoute, IconAlertTriangle } from '@tabler/icons-react' -import RoadmapProgressBar from './roadmap/RoadmapProgressBar' -import { - fetchRoadmapByTicketId, - fetchRoadmapNodes, - computeProgress, - isNodeOverdue, - NODE_KIND, - NODE_STATUS, - ROADMAP_STATUS, -} from '../lib/roadmaps' - -/** - * Roadmap-Tracking im Projektticket: Fortschritt, Mini-Uebersicht und Link zum Board. - * Rendert nichts, wenn zum Ticket keine Roadmap existiert. - */ -export default function TicketRoadmapPanel({ ticket }) { - const [roadmap, setRoadmap] = useState(null) - const [nodes, setNodes] = useState([]) - const [loaded, setLoaded] = useState(false) - - useEffect(() => { - let cancelled = false - async function load() { - try { - const doc = await fetchRoadmapByTicketId(ticket.$id) - if (cancelled) return - setRoadmap(doc) - if (doc) setNodes(await fetchRoadmapNodes(doc.$id)) - } catch { - /* Panel ist optional */ - } finally { - if (!cancelled) setLoaded(true) - } - } - load() - return () => { cancelled = true } - }, [ticket.$id]) - - if (!loaded || !roadmap) return null - - const progress = computeProgress(nodes) - const relevant = nodes.filter((n) => n.kind !== NODE_KIND.START) - const counts = { - open: relevant.filter((n) => (n.status || NODE_STATUS.OPEN) === NODE_STATUS.OPEN).length, - inProgress: relevant.filter((n) => n.status === NODE_STATUS.IN_PROGRESS).length, - done: relevant.filter((n) => n.status === NODE_STATUS.DONE).length, - } - const nextDue = relevant - .filter((n) => n.deadline && n.status !== NODE_STATUS.DONE) - .sort((a, b) => a.deadline.localeCompare(b.deadline)) - .slice(0, 3) - const closedButActive = ticket.status === 'Closed' && roadmap.status === ROADMAP_STATUS.ACTIVE - - return ( -
-
-
- Roadmap: {roadmap.title} -
- Zur Roadmap -
- - - -
- Offen: {counts.open} - In Arbeit: {counts.inProgress} - Fertig: {counts.done} - {roadmap.status === ROADMAP_STATUS.COMPLETED && Plan abgeschlossen} -
- - {nextDue.length > 0 && ( -
- Naechste Deadlines: - {nextDue.map((n, i) => ( - - {i > 0 && ' · '}{n.title} ({n.deadline.split('-').reverse().join('.')}) - - ))} -
- )} - - {closedButActive && ( -
- - Ticket ist geschlossen, aber der Plan ist erst zu {progress.percent}% fertig. -
- )} -
- ) -} diff --git a/src/components/roadmap/NodeDetailPanel.jsx b/src/components/roadmap/NodeDetailPanel.jsx deleted file mode 100644 index dac9a00..0000000 --- a/src/components/roadmap/NodeDetailPanel.jsx +++ /dev/null @@ -1,166 +0,0 @@ -import { useEffect, useState } from 'react' -import { IconTrash, IconX } from '@tabler/icons-react' -import { Card, Button, Field } from '../ui' -import { useEmployees } from '../../hooks/useEmployees' -import { NODE_KIND, NODE_STATUS, NODE_STATUS_LABELS } from '../../lib/roadmaps' - -const STATUS_ORDER = [NODE_STATUS.OPEN, NODE_STATUS.IN_PROGRESS, NODE_STATUS.DONE] - -/** - * Seitliches Detail-Panel fuer den ausgewaehlten Knoten: - * Titel, Status, Zustaendiger, Deadline, Notizen, eingehende Verbindungen. - */ -export default function NodeDetailPanel({ - node, - nodes, - readOnly, - onClose, - onSave, - onSetStatus, - onDelete, - onDisconnect, -}) { - const { employees } = useEmployees() - const [form, setForm] = useState({ title: '', assignedTo: '', deadline: '', notes: '' }) - const [saving, setSaving] = useState(false) - const [error, setError] = useState('') - - useEffect(() => { - setForm({ - title: node.title || '', - assignedTo: node.assignedTo || '', - deadline: node.deadline || '', - notes: node.notes || '', - }) - setError('') - }, [node.$id]) // eslint-disable-line react-hooks/exhaustive-deps - - const isStart = node.kind === NODE_KIND.START - const incoming = (node.sourceIds || []) - .map((id) => nodes.find((n) => n.$id === id)) - .filter(Boolean) - - const handleSave = async () => { - if (!form.title.trim()) { - setError('Titel darf nicht leer sein.') - return - } - setSaving(true) - setError('') - const employee = employees.find((e) => e.userId === form.assignedTo) - const result = await onSave(node, { - title: form.title.trim(), - assignedTo: form.assignedTo, - assignedName: employee?.displayName || '', - deadline: form.deadline, - notes: form.notes, - }) - setSaving(false) - if (!result.success) setError(result.error) - } - - return ( - } - > -
- - setForm((f) => ({ ...f, title: e.target.value }))} - /> - - - {!isStart && ( - -
- {STATUS_ORDER.map((status) => ( - - ))} -
-
- )} - - {!isStart && ( - - - - )} - - {!isStart && ( - - setForm((f) => ({ ...f, deadline: e.target.value }))} - /> - - )} - - -