feat(roadmaps): Hooks, Routing, Sidebar-Tab und Übersichtsseite
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
135
src/hooks/useRoadmapNodes.js
Normal file
135
src/hooks/useRoadmapNodes.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { databases, DATABASE_ID, COLLECTIONS, ID, isDemoMode } 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 || isDemoMode) 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 }) => {
|
||||
if (isDemoMode) return { success: false, error: 'Demo-Modus' }
|
||||
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) => {
|
||||
if (isDemoMode) return { success: false, error: 'Demo-Modus' }
|
||||
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 (isDemoMode) return { success: false, error: 'Demo-Modus' }
|
||||
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 (isDemoMode) return { success: false, error: 'Demo-Modus' }
|
||||
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) => {
|
||||
if (isDemoMode) return { success: false, error: 'Demo-Modus' }
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user