136 lines
5.0 KiB
JavaScript
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,
|
|
}
|
|
}
|