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 }) {
-
- {percent}%
- {!compact && {done} von {total} fertig }
-
-
-
= 100 ? 'var(--ok, #22c55e)' : 'var(--accent, #3b82f6)',
- transition: 'width 0.3s ease',
- }}
- />
-
-
- )
-}
diff --git a/src/hooks/useRoadmapNodes.js b/src/hooks/useRoadmapNodes.js
deleted file mode 100644
index 37e96a5..0000000
--- a/src/hooks/useRoadmapNodes.js
+++ /dev/null
@@ -1,130 +0,0 @@
-import { useState, useCallback } from 'react'
-import { databases, DATABASE_ID, COLLECTIONS, ID } from '../lib/appwrite'
-import {
- fetchRoadmapNodes,
- logNodeStatusWorksheet,
- allGoalsDone,
- NODE_KIND,
- NODE_STATUS,
-} from '../lib/roadmaps'
-
-export function useRoadmapNodes() {
- const [nodes, setNodes] = useState([])
- const [loading, setLoading] = useState(false)
- const [error, setError] = useState(null)
-
- const fetchNodes = useCallback(async (roadmapId) => {
- if (!roadmapId) return []
- setLoading(true)
- setError(null)
- try {
- const docs = await fetchRoadmapNodes(roadmapId)
- setNodes(docs)
- return docs
- } catch (err) {
- setError(err.message)
- return []
- } finally {
- setLoading(false)
- }
- }, [])
-
- const createNode = async (roadmapId, { title, column, row, kind }) => {
- try {
- const now = new Date().toISOString()
- const doc = await databases.createDocument(DATABASE_ID, COLLECTIONS.ROADMAP_NODES, ID.unique(), {
- roadmapId,
- title: title || 'Neuer Schritt',
- kind: kind || NODE_KIND.STEP,
- column,
- row,
- status: NODE_STATUS.OPEN,
- createdAt: now,
- updatedAt: now,
- })
- setNodes((prev) => [...prev, doc])
- return { success: true, data: doc }
- } catch (err) {
- return { success: false, error: err.message }
- }
- }
-
- const updateNode = async (nodeId, data) => {
- try {
- const doc = await databases.updateDocument(DATABASE_ID, COLLECTIONS.ROADMAP_NODES, nodeId, {
- ...data,
- updatedAt: new Date().toISOString(),
- })
- setNodes((prev) => prev.map((n) => (n.$id === nodeId ? doc : n)))
- return { success: true, data: doc }
- } catch (err) {
- return { success: false, error: err.message }
- }
- }
-
- /** Knoten loeschen inkl. Bereinigung eingehender Kanten bei den Nachfolgern. */
- const deleteNode = async (node) => {
- if (node.kind === NODE_KIND.START) {
- return { success: false, error: 'Der Start-Knoten kann nicht geloescht werden.' }
- }
- if (node.kind === NODE_KIND.GOAL && nodes.filter((n) => n.kind === NODE_KIND.GOAL).length <= 1) {
- return { success: false, error: 'Ein Plan braucht mindestens ein Ziel.' }
- }
- try {
- const successors = nodes.filter((n) => (n.sourceIds || []).includes(node.$id))
- await Promise.all(successors.map((n) =>
- databases.updateDocument(DATABASE_ID, COLLECTIONS.ROADMAP_NODES, n.$id, {
- sourceIds: (n.sourceIds || []).filter((id) => id !== node.$id),
- updatedAt: new Date().toISOString(),
- })
- ))
- await databases.deleteDocument(DATABASE_ID, COLLECTIONS.ROADMAP_NODES, node.$id)
- setNodes((prev) => prev
- .filter((n) => n.$id !== node.$id)
- .map((n) => ((n.sourceIds || []).includes(node.$id)
- ? { ...n, sourceIds: n.sourceIds.filter((id) => id !== node.$id) }
- : n))
- )
- return { success: true }
- } catch (err) {
- return { success: false, error: err.message }
- }
- }
-
- /** Kante anlegen: immer vorwaerts (Zielspalte > Quellspalte), keine Duplikate. */
- const connectNodes = async (source, target) => {
- if (source.$id === target.$id) return { success: false, error: 'Knoten kann nicht mit sich selbst verbunden werden.' }
- if (target.column <= source.column) {
- return { success: false, error: 'Verbindungen laufen immer vorwaerts (nach rechts).' }
- }
- if ((target.sourceIds || []).includes(source.$id)) {
- return { success: false, error: 'Diese Verbindung existiert bereits.' }
- }
- return updateNode(target.$id, { sourceIds: [...(target.sourceIds || []), source.$id] })
- }
-
- const disconnectNodes = async (sourceId, target) => {
- return updateNode(target.$id, { sourceIds: (target.sourceIds || []).filter((id) => id !== sourceId) })
- }
-
- /** Statuswechsel inkl. Worksheet-Log am Projektticket; meldet, ob alle Ziele fertig sind. */
- const setNodeStatus = async (roadmap, node, newStatus, user) => {
- const oldStatus = node.status || NODE_STATUS.OPEN
- if (oldStatus === newStatus) return { success: true, allGoalsDone: false }
- const result = await updateNode(node.$id, { status: newStatus })
- if (!result.success) return result
- try {
- await logNodeStatusWorksheet(roadmap, node, oldStatus, newStatus, user)
- } catch {
- /* Verlaufslog ist optional - Statuswechsel bleibt gueltig */
- }
- const updatedNodes = nodes.map((n) => (n.$id === node.$id ? { ...n, status: newStatus } : n))
- return { success: true, data: result.data, allGoalsDone: allGoalsDone(updatedNodes) }
- }
-
- return {
- nodes, loading, error, setNodes,
- fetchNodes, createNode, updateNode, deleteNode,
- connectNodes, disconnectNodes, setNodeStatus,
- }
-}
diff --git a/src/hooks/useRoadmaps.js b/src/hooks/useRoadmaps.js
deleted file mode 100644
index 5ece0a6..0000000
--- a/src/hooks/useRoadmaps.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import { useState, useCallback } from 'react'
-import { databases, DATABASE_ID, COLLECTIONS, Query } from '../lib/appwrite'
-import {
- createRoadmapPlan,
- deleteRoadmapPlan,
- completeRoadmap,
- fetchRoadmapByTicketId,
-} from '../lib/roadmaps'
-
-export function useRoadmaps() {
- const [roadmaps, setRoadmaps] = useState([])
- const [loading, setLoading] = useState(false)
- const [error, setError] = useState(null)
-
- const fetchAll = useCallback(async () => {
- setLoading(true)
- setError(null)
- try {
- const response = await databases.listDocuments(
- DATABASE_ID, COLLECTIONS.ROADMAPS,
- [Query.orderDesc('$createdAt'), Query.limit(500)]
- )
- setRoadmaps(response.documents)
- return response.documents
- } catch (err) {
- setError(err.message)
- return []
- } finally {
- setLoading(false)
- }
- }, [])
-
- const fetchById = useCallback(async (roadmapId) => {
- if (!roadmapId) return null
- try {
- return await databases.getDocument(DATABASE_ID, COLLECTIONS.ROADMAPS, roadmapId)
- } catch {
- return null
- }
- }, [])
-
- const fetchByTicketId = useCallback(async (ticketId) => {
- try {
- return await fetchRoadmapByTicketId(ticketId)
- } catch {
- return null
- }
- }, [])
-
- const createRoadmap = async (project, data, user) => {
- try {
- const result = await createRoadmapPlan(project, data, user)
- setRoadmaps((prev) => [result.roadmap, ...prev])
- return { success: true, data: result }
- } catch (err) {
- return { success: false, error: err.message }
- }
- }
-
- const updateRoadmap = async (roadmapId, data) => {
- try {
- const doc = await databases.updateDocument(DATABASE_ID, COLLECTIONS.ROADMAPS, roadmapId, {
- ...data,
- updatedAt: new Date().toISOString(),
- })
- setRoadmaps((prev) => prev.map((r) => (r.$id === roadmapId ? doc : r)))
- return { success: true, data: doc }
- } catch (err) {
- return { success: false, error: err.message }
- }
- }
-
- const deleteRoadmap = async (roadmap, user) => {
- try {
- await deleteRoadmapPlan(roadmap, user)
- setRoadmaps((prev) => prev.filter((r) => r.$id !== roadmap.$id))
- return { success: true }
- } catch (err) {
- return { success: false, error: err.message }
- }
- }
-
- const completePlan = async (roadmap, user) => {
- try {
- const updated = await completeRoadmap(roadmap, user)
- setRoadmaps((prev) => prev.map((r) => (r.$id === updated.$id ? updated : r)))
- return { success: true, data: updated }
- } catch (err) {
- return { success: false, error: err.message }
- }
- }
-
- return { roadmaps, loading, error, fetchAll, fetchById, fetchByTicketId, createRoadmap, updateRoadmap, deleteRoadmap, completePlan }
-}
diff --git a/src/lib/appwrite.js b/src/lib/appwrite.js
index 88cc5da..9a6bce5 100644
--- a/src/lib/appwrite.js
+++ b/src/lib/appwrite.js
@@ -35,12 +35,8 @@ export const COLLECTIONS = {
LEADS: 'leads',
LEAD_SETTINGS: 'leadSettings',
LEAD_PIPELINE: 'leadPipeline',
- ROADMAPS: 'roadmaps',
- ROADMAP_NODES: 'roadmapNodes',
}
-export const PROJECT_TICKET_TYPE = 'Project'
-
/** Prüft ob vermutlich eine Session existiert (cookieFallback oder Same-Origin-Cookie). */
export function hasAppwriteSession() {
if (typeof window === 'undefined' || !projectId) return false
diff --git a/src/lib/roadmaps.js b/src/lib/roadmaps.js
deleted file mode 100644
index 8047629..0000000
--- a/src/lib/roadmaps.js
+++ /dev/null
@@ -1,292 +0,0 @@
-import { databases, DATABASE_ID, COLLECTIONS, PROJECT_TICKET_TYPE, Query, ID } from './appwrite'
-
-/**
- * Roadmaps: projektspezifische Plaene als Steckbrett (Spalten-Raster).
- * Ein Dokument pro Knoten; Kanten als sourceIds (Vorgaenger) am Zielknoten.
- * Genau 1 aktiver Plan pro Projekt (Pruefung hier, nicht per Index erzwingbar).
- * Jeder Plan erzeugt automatisch ein Projektticket (Vorbild: createAcquisitionTicket in leads.js).
- */
-
-export const ROADMAP_STATUS = { ACTIVE: 'active', COMPLETED: 'completed' }
-export const NODE_STATUS = { OPEN: 'open', IN_PROGRESS: 'in_progress', DONE: 'done' }
-export const NODE_KIND = { START: 'start', STEP: 'step', GOAL: 'goal' }
-
-export const NODE_STATUS_LABELS = {
- open: 'Offen',
- in_progress: 'In Arbeit',
- done: 'Fertig',
-}
-
-/** Fortschritt in Prozent: fertige Knoten / alle Knoten ausser Start. */
-export function computeProgress(nodes) {
- const relevant = (nodes || []).filter((n) => n.kind !== NODE_KIND.START)
- if (!relevant.length) return { percent: 0, done: 0, total: 0 }
- const done = relevant.filter((n) => n.status === NODE_STATUS.DONE).length
- return { percent: Math.round((done / relevant.length) * 100), done, total: relevant.length }
-}
-
-export function allGoalsDone(nodes) {
- const goals = (nodes || []).filter((n) => n.kind === NODE_KIND.GOAL)
- return goals.length > 0 && goals.every((n) => n.status === NODE_STATUS.DONE)
-}
-
-export function isNodeOverdue(node) {
- if (!node?.deadline || node.status === NODE_STATUS.DONE) return false
- return node.deadline < new Date().toISOString().slice(0, 10)
-}
-
-/** Aktiver Plan eines Projekts (oder null). */
-export async function fetchActiveRoadmapByProject(projectId) {
- const res = await databases.listDocuments(DATABASE_ID, COLLECTIONS.ROADMAPS, [
- Query.equal('projectId', projectId),
- Query.equal('status', ROADMAP_STATUS.ACTIVE),
- Query.limit(1),
- ])
- return res.documents[0] || null
-}
-
-export async function fetchRoadmapByTicketId(ticketId) {
- if (!ticketId) return null
- const res = await databases.listDocuments(DATABASE_ID, COLLECTIONS.ROADMAPS, [
- Query.equal('ticketId', ticketId),
- Query.limit(1),
- ])
- return res.documents[0] || null
-}
-
-export async function fetchRoadmapNodes(roadmapId) {
- const res = await databases.listDocuments(DATABASE_ID, COLLECTIONS.ROADMAP_NODES, [
- Query.equal('roadmapId', roadmapId),
- Query.limit(500),
- ])
- return res.documents
-}
-
-async function nextWoid() {
- try {
- const res = await databases.listDocuments(DATABASE_ID, COLLECTIONS.WORKORDERS, [
- Query.orderDesc('$createdAt'),
- Query.limit(200),
- ])
- const nums = res.documents.map((d) => parseInt(d.woid)).filter((n) => !isNaN(n) && n > 0)
- return String((nums.length ? Math.max(...nums) : 9999) + 1)
- } catch {
- return String(10000)
- }
-}
-
-async function nextWsid() {
- try {
- const res = await databases.listDocuments(DATABASE_ID, COLLECTIONS.WORKSHEETS, [
- Query.orderDesc('wsid'),
- Query.limit(1),
- ])
- const highest = parseInt(res.documents[0]?.wsid)
- return String(isNaN(highest) ? 100000 : highest + 1)
- } catch {
- return String(100000)
- }
-}
-
-/** Automatisches Projektticket fuer einen neuen Plan. */
-async function createRoadmapTicket(roadmap, project, user) {
- const woid = await nextWoid()
- const data = {
- topic: `Projekt: ${project.projectName}`,
- status: 'Open',
- priority: 2,
- woid,
- type: PROJECT_TICKET_TYPE,
- systemType: 'n/a',
- serviceType: 'Remote',
- assignedTo: user?.$id || '',
- details:
- `Automatisch erstelltes Projektticket fuer die Roadmap "${roadmap.title}" ` +
- `zum Projekt "${project.projectName}".\n\n` +
- `Der Planfortschritt wird hier getrackt: Jede Statusaenderung eines Roadmap-Knotens ` +
- `erzeugt einen Verlaufseintrag (Worksheet). Sind alle Ziel-Knoten fertig, wird das ` +
- `Ticket geschlossen und der Plan archiviert.`,
- createdAt: new Date().toISOString(),
- }
- if (project.customerId) {
- data.customerId = project.customerId
- if (roadmap.customerName) data.customerName = roadmap.customerName
- }
- return databases.createDocument(DATABASE_ID, COLLECTIONS.WORKORDERS, ID.unique(), data)
-}
-
-/**
- * Legt einen neuen Plan an: Roadmap-Dokument + Start-Knoten + Ziel-Knoten + Auto-Ticket.
- * Wirft, wenn das Projekt bereits einen aktiven Plan hat.
- */
-export async function createRoadmapPlan(project, { title, goalTitle }, user) {
- const existing = await fetchActiveRoadmapByProject(project.$id)
- if (existing) {
- throw new Error(`Das Projekt "${project.projectName}" hat bereits einen aktiven Plan.`)
- }
-
- let customerName = ''
- if (project.customerId) {
- try {
- const customer = await databases.getDocument(DATABASE_ID, COLLECTIONS.CUSTOMERS, project.customerId)
- customerName = customer.name || ''
- } catch {
- /* Kunde optional */
- }
- }
-
- const now = new Date().toISOString()
- const columnCount = 3
- const roadmap = await databases.createDocument(DATABASE_ID, COLLECTIONS.ROADMAPS, ID.unique(), {
- projectId: project.$id,
- projectName: project.projectName || '',
- customerId: project.customerId || '',
- customerName,
- title: title || `Plan: ${project.projectName}`,
- status: ROADMAP_STATUS.ACTIVE,
- columnCount,
- createdBy: user?.$id || '',
- createdAt: now,
- updatedAt: now,
- })
-
- await Promise.all([
- databases.createDocument(DATABASE_ID, COLLECTIONS.ROADMAP_NODES, ID.unique(), {
- roadmapId: roadmap.$id,
- title: 'Start',
- kind: NODE_KIND.START,
- column: 0,
- row: 0,
- status: NODE_STATUS.OPEN,
- createdAt: now,
- updatedAt: now,
- }),
- databases.createDocument(DATABASE_ID, COLLECTIONS.ROADMAP_NODES, ID.unique(), {
- roadmapId: roadmap.$id,
- title: goalTitle || 'Ziel',
- kind: NODE_KIND.GOAL,
- column: columnCount - 1,
- row: 0,
- status: NODE_STATUS.OPEN,
- createdAt: now,
- updatedAt: now,
- }),
- ])
-
- let ticket = null
- try {
- ticket = await createRoadmapTicket(roadmap, project, user)
- await databases.updateDocument(DATABASE_ID, COLLECTIONS.ROADMAPS, roadmap.$id, {
- ticketId: ticket.$id,
- woid: ticket.woid,
- })
- roadmap.ticketId = ticket.$id
- roadmap.woid = ticket.woid
- } catch {
- /* Ticket ist optional - Plan bleibt bestehen */
- }
- return { roadmap, ticket }
-}
-
-/** Verlaufseintrag (Worksheet-Kommentar) am Projektticket, ohne den Ticket-Status zu aendern. */
-export async function logRoadmapWorksheet(roadmap, detailsText, user) {
- if (!roadmap?.ticketId) return null
- let ticket
- try {
- ticket = await databases.getDocument(DATABASE_ID, COLLECTIONS.WORKORDERS, roadmap.ticketId)
- } catch {
- return null // Ticket geloescht -> kein Log
- }
- const today = new Date().toISOString().slice(0, 10)
- const wsid = await nextWsid()
- return databases.createDocument(DATABASE_ID, COLLECTIONS.WORKSHEETS, ID.unique(), {
- wsid,
- woid: ticket.woid || roadmap.woid || '',
- workorderId: ticket.$id,
- employeeId: user?.$id || 'system',
- employeeName: user?.name || user?.email || 'System',
- serviceType: 'Remote',
- oldStatus: ticket.status,
- newStatus: ticket.status,
- totalTime: 0,
- startDate: today,
- endDate: today,
- details: detailsText,
- isComment: true,
- createdAt: new Date().toISOString(),
- })
-}
-
-/** Knoten-Statuswechsel im Ticketverlauf dokumentieren. */
-export async function logNodeStatusWorksheet(roadmap, node, oldStatus, newStatus, user) {
- const from = NODE_STATUS_LABELS[oldStatus] || oldStatus || '-'
- const to = NODE_STATUS_LABELS[newStatus] || newStatus
- return logRoadmapWorksheet(roadmap, `Roadmap-Knoten "${node.title}": ${from} -> ${to}`, user)
-}
-
-/**
- * Plan abschliessen: Roadmap archivieren, Ticket schliessen (falls noch offen)
- * und den Abschluss im Verlauf dokumentieren.
- */
-export async function completeRoadmap(roadmap, user) {
- const now = new Date().toISOString()
- const updated = await databases.updateDocument(DATABASE_ID, COLLECTIONS.ROADMAPS, roadmap.$id, {
- status: ROADMAP_STATUS.COMPLETED,
- completedAt: now,
- updatedAt: now,
- })
- if (roadmap.ticketId) {
- try {
- const ticket = await databases.getDocument(DATABASE_ID, COLLECTIONS.WORKORDERS, roadmap.ticketId)
- if (ticket.status !== 'Closed') {
- const wsid = await nextWsid()
- const today = now.slice(0, 10)
- await databases.createDocument(DATABASE_ID, COLLECTIONS.WORKSHEETS, ID.unique(), {
- wsid,
- woid: ticket.woid || '',
- workorderId: ticket.$id,
- employeeId: user?.$id || 'system',
- employeeName: user?.name || user?.email || 'System',
- serviceType: 'Remote',
- oldStatus: ticket.status,
- newStatus: 'Closed',
- totalTime: 0,
- startDate: today,
- endDate: today,
- details: `Roadmap "${roadmap.title}" abgeschlossen: alle Ziel-Knoten fertig. Ticket automatisch geschlossen.`,
- isComment: true,
- createdAt: now,
- })
- await databases.updateDocument(DATABASE_ID, COLLECTIONS.WORKORDERS, ticket.$id, { status: 'Closed' })
- } else {
- await logRoadmapWorksheet({ ...roadmap, status: ROADMAP_STATUS.COMPLETED }, `Roadmap "${roadmap.title}" abgeschlossen (Ticket war bereits geschlossen).`, user)
- }
- } catch {
- /* Ticket fehlt -> nur archivieren */
- }
- }
- return updated
-}
-
-/**
- * Aktiven Plan loeschen (destruktiv): alle Knoten + Roadmap entfernen,
- * Auto-Ticket auf Cancelled setzen. Abgeschlossene Plaene werden nie geloescht.
- */
-export async function deleteRoadmapPlan(roadmap, user) {
- const nodes = await fetchRoadmapNodes(roadmap.$id)
- await logRoadmapWorksheet(roadmap, `Roadmap "${roadmap.title}" geloescht. Ticket wird abgebrochen.`, user)
- for (const node of nodes) {
- await databases.deleteDocument(DATABASE_ID, COLLECTIONS.ROADMAP_NODES, node.$id)
- }
- await databases.deleteDocument(DATABASE_ID, COLLECTIONS.ROADMAPS, roadmap.$id)
- if (roadmap.ticketId) {
- try {
- const ticket = await databases.getDocument(DATABASE_ID, COLLECTIONS.WORKORDERS, roadmap.ticketId)
- if (ticket.status !== 'Closed' && ticket.status !== 'Cancelled') {
- await databases.updateDocument(DATABASE_ID, COLLECTIONS.WORKORDERS, ticket.$id, { status: 'Cancelled' })
- }
- } catch {
- /* Ticket fehlt -> nichts zu tun */
- }
- }
-}
diff --git a/src/pages/RoadmapDetailPage.jsx b/src/pages/RoadmapDetailPage.jsx
deleted file mode 100644
index 87875da..0000000
--- a/src/pages/RoadmapDetailPage.jsx
+++ /dev/null
@@ -1,205 +0,0 @@
-import { useCallback, useEffect, useState } from 'react'
-import { Link, useNavigate, useParams } from 'react-router-dom'
-import { IconArrowLeft, IconColumnInsertRight, IconTicket, IconAlertTriangle } from '@tabler/icons-react'
-import { PageHeader, Card, Button, Badge } from '../components/ui'
-import RoadmapBoard from '../components/roadmap/RoadmapBoard'
-import NodeDetailPanel from '../components/roadmap/NodeDetailPanel'
-import RoadmapProgressBar from '../components/roadmap/RoadmapProgressBar'
-import { useAuth } from '../context/AuthContext'
-import { useRoadmaps } from '../hooks/useRoadmaps'
-import { useRoadmapNodes } from '../hooks/useRoadmapNodes'
-import { databases, DATABASE_ID, COLLECTIONS } from '../lib/appwrite'
-import { computeProgress, NODE_KIND, ROADMAP_STATUS } from '../lib/roadmaps'
-
-export default function RoadmapDetailPage() {
- const { id } = useParams()
- const navigate = useNavigate()
- const { user } = useAuth()
- const { fetchById, updateRoadmap, completePlan } = useRoadmaps()
- const {
- nodes, fetchNodes, createNode, updateNode, deleteNode,
- connectNodes, disconnectNodes, setNodeStatus,
- } = useRoadmapNodes()
-
- const [roadmap, setRoadmap] = useState(null)
- const [ticketStatus, setTicketStatus] = useState(null)
- const [selectedNode, setSelectedNode] = useState(null)
- const [notFound, setNotFound] = useState(false)
-
- const load = useCallback(async () => {
- const doc = await fetchById(id)
- if (!doc) {
- setNotFound(true)
- return
- }
- setRoadmap(doc)
- await fetchNodes(doc.$id)
- if (doc.ticketId) {
- try {
- const ticket = await databases.getDocument(DATABASE_ID, COLLECTIONS.WORKORDERS, doc.ticketId)
- setTicketStatus(ticket.status)
- } catch {
- setTicketStatus(null)
- }
- }
- }, [id, fetchById, fetchNodes])
-
- useEffect(() => { load() }, [load])
-
- // Teamarbeit: bei Fenster-Fokus neu laden (einfacher Schutz gegen veraltete Ansicht)
- useEffect(() => {
- const onFocus = () => load()
- window.addEventListener('focus', onFocus)
- return () => window.removeEventListener('focus', onFocus)
- }, [load])
-
- // Ausgewaehlten Knoten mit frischen Daten aus der Liste spiegeln
- useEffect(() => {
- if (!selectedNode) return
- const fresh = nodes.find((n) => n.$id === selectedNode.$id)
- if (!fresh) setSelectedNode(null)
- else if (fresh !== selectedNode) setSelectedNode(fresh)
- }, [nodes]) // eslint-disable-line react-hooks/exhaustive-deps
-
- if (notFound) {
- return (
-
-
-
navigate('/roadmaps')}> Zur Uebersicht
-
- )
- }
- if (!roadmap) {
- return
- }
-
- const readOnly = roadmap.status === ROADMAP_STATUS.COMPLETED
- const progress = computeProgress(nodes)
- const ticketClosedButActive = !readOnly && ticketStatus === 'Closed'
-
- const handleAddNode = async (column, kind, rowCount) => {
- const result = await createNode(roadmap.$id, {
- title: kind === NODE_KIND.GOAL ? 'Neues Ziel' : 'Neuer Schritt',
- column,
- row: rowCount,
- kind,
- })
- if (result.success) setSelectedNode(result.data)
- else window.alert(result.error)
- }
-
- const handleConnect = async (source, target) => {
- const result = await connectNodes(source, target)
- if (!result.success) window.alert(result.error)
- }
-
- const handleSaveNode = async (node, data) => updateNode(node.$id, data)
-
- const handleSetStatus = async (node, status) => {
- const result = await setNodeStatus(roadmap, node, status, user)
- if (!result.success) {
- window.alert(result.error)
- return
- }
- if (result.allGoalsDone) {
- const ok = window.confirm(
- 'Alle Ziel-Knoten sind fertig!\n\n' +
- 'Plan abschliessen und archivieren? Das Projektticket wird dabei geschlossen.'
- )
- if (ok) {
- const done = await completePlan(roadmap, user)
- if (done.success) setRoadmap(done.data)
- else window.alert(`Abschliessen fehlgeschlagen: ${done.error}`)
- }
- }
- }
-
- const handleDeleteNode = async (node) => {
- if (!window.confirm(`Knoten "${node.title}" loeschen?`)) return
- const result = await deleteNode(node)
- if (!result.success) window.alert(result.error)
- else setSelectedNode(null)
- }
-
- const handleDisconnect = async (sourceId, target) => {
- const result = await disconnectNodes(sourceId, target)
- if (!result.success) window.alert(result.error)
- }
-
- /** Neue Phase vor den Zielen einfuegen: Ziel-Knoten ruecken eine Spalte nach rechts. */
- const handleAddColumn = async () => {
- const newCount = (roadmap.columnCount || 3) + 1
- if (newCount > 50) return
- const goals = nodes.filter((n) => n.kind === NODE_KIND.GOAL)
- for (const goal of goals) {
- await updateNode(goal.$id, { column: newCount - 1 })
- }
- const result = await updateRoadmap(roadmap.$id, { columnCount: newCount })
- if (result.success) setRoadmap(result.data)
- else window.alert(result.error)
- }
-
- return (
-
-
-
- {readOnly && Abgeschlossen }
- {roadmap.woid && (
-
- Ticket {roadmap.woid}
-
- )}
- {!readOnly && (
-
- Spalte
-
- )}
- navigate('/roadmaps')}> Zurueck
-
- }
- />
-
- {ticketClosedButActive && (
-
-
-
-
- Das Projektticket wurde manuell geschlossen, der Plan ist aber erst zu {progress.percent}% fertig.
- Statusaenderungen werden weiterhin im Ticketverlauf dokumentiert.
-
-
-
- )}
-
-
-
-
-
- {selectedNode && (
- setSelectedNode(null)}
- onSave={handleSaveNode}
- onSetStatus={handleSetStatus}
- onDelete={handleDeleteNode}
- onDisconnect={handleDisconnect}
- />
- )}
-
-
- )
-}
diff --git a/src/pages/RoadmapsPage.jsx b/src/pages/RoadmapsPage.jsx
deleted file mode 100644
index dbe876d..0000000
--- a/src/pages/RoadmapsPage.jsx
+++ /dev/null
@@ -1,221 +0,0 @@
-import { useEffect, useMemo, useState } from 'react'
-import { Link, useNavigate } from 'react-router-dom'
-import { FaTimes } from 'react-icons/fa'
-import { IconRoute, IconPlus, IconTrash, IconTicket } from '@tabler/icons-react'
-import { PageHeader, Card, Button, Badge, EmptyState, Field } from '../components/ui'
-import { useAuth } from '../context/AuthContext'
-import { useRoadmaps } from '../hooks/useRoadmaps'
-import { useWebsiteProjects } from '../hooks/useWebsiteProjects'
-import { fetchRoadmapNodes, computeProgress, ROADMAP_STATUS } from '../lib/roadmaps'
-import RoadmapProgress from '../components/roadmap/RoadmapProgressBar'
-
-export default function RoadmapsPage() {
- const { user } = useAuth()
- const navigate = useNavigate()
- const { roadmaps, loading, fetchAll, createRoadmap, deleteRoadmap } = useRoadmaps()
- const { fetchAllProjects } = useWebsiteProjects()
-
- const [projects, setProjects] = useState([])
- const [progressById, setProgressById] = useState({})
- const [showCreate, setShowCreate] = useState(false)
- const [form, setForm] = useState({ projectId: '', title: '', goalTitle: '' })
- const [creating, setCreating] = useState(false)
- const [error, setError] = useState('')
-
- useEffect(() => {
- let cancelled = false
- async function load() {
- const [allProjects, allRoadmaps] = await Promise.all([fetchAllProjects(), fetchAll()])
- if (cancelled) return
- setProjects(allProjects.filter((p) => !p.isTemplate && !p.archived))
- const active = allRoadmaps.filter((r) => r.status === ROADMAP_STATUS.ACTIVE)
- const entries = await Promise.all(active.map(async (r) => {
- try {
- return [r.$id, computeProgress(await fetchRoadmapNodes(r.$id))]
- } catch {
- return [r.$id, null]
- }
- }))
- if (!cancelled) setProgressById(Object.fromEntries(entries.filter(([, p]) => p)))
- }
- load()
- return () => { cancelled = true }
- }, [fetchAll, fetchAllProjects])
-
- const activeRoadmaps = roadmaps.filter((r) => r.status === ROADMAP_STATUS.ACTIVE)
- const completedRoadmaps = roadmaps.filter((r) => r.status === ROADMAP_STATUS.COMPLETED)
-
- // Nur Projekte ohne aktiven Plan anbieten (genau 1 aktiver Plan pro Projekt)
- const availableProjects = useMemo(() => {
- const withActivePlan = new Set(activeRoadmaps.map((r) => r.projectId))
- return projects.filter((p) => !withActivePlan.has(p.$id))
- }, [projects, activeRoadmaps])
-
- const openCreate = () => {
- setForm({ projectId: availableProjects[0]?.$id || '', title: '', goalTitle: '' })
- setError('')
- setShowCreate(true)
- }
-
- const handleCreate = async (e) => {
- e.preventDefault()
- const project = projects.find((p) => p.$id === form.projectId)
- if (!project) {
- setError('Bitte ein Projekt auswaehlen.')
- return
- }
- setCreating(true)
- setError('')
- const result = await createRoadmap(project, { title: form.title.trim(), goalTitle: form.goalTitle.trim() }, user)
- setCreating(false)
- if (!result.success) {
- setError(result.error)
- return
- }
- setShowCreate(false)
- navigate(`/roadmaps/${result.data.roadmap.$id}`)
- }
-
- const handleDelete = async (roadmap) => {
- const ok = window.confirm(
- `Plan "${roadmap.title}" wirklich loeschen?\n\n` +
- `Alle Knoten werden entfernt und das Projektticket (WOID ${roadmap.woid || '-'}) wird abgebrochen.`
- )
- if (!ok) return
- const result = await deleteRoadmap(roadmap, user)
- if (!result.success) window.alert(`Loeschen fehlgeschlagen: ${result.error}`)
- }
-
- const renderRoadmapCard = (roadmap) => {
- const progress = progressById[roadmap.$id]
- return (
-