Files
tickte-system/src/hooks/useRoadmapNodes.js
2026-07-22 19:55:47 +00:00

136 lines
5.0 KiB
JavaScript

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,
}
}