();
+
+
+ //--- GroupInit changed
+ useEffect(() => {
+
+ if (groupIntial) {
+ const { id, ...rest } = groupIntial;
+ setId(id);
+ reset({ ...emptyGroup, ...rest })
+ } else {
+ reset({ ...emptyGroup })
+ }
+ }, [groupIntial]);
+
+ //--- Parent Group hat sich geändert
+ useEffect(() => {
+ parentGroup ? setValue("parentId", parentGroup.id) : setValue("parentId", undefined);
+ }, [parentGroup])
+
+ const onSubmit = async (data: GroupCreate) => {
+
+ setError(undefined);
+
+ const { id: id1, ...sendData } = data; // Id entfernen!
+
+ let response = id ? await api_groups_update(id, sendData) : await api_groups_create(data);
+ if (response.success) {
+ reset({ ...emptyGroup, ...response.data })
+ setId(response.data.id);
+ setToast("Erfolgreich gespeichert!");
+ router.back();
+ } else {
+ setToast("Fehler beim Speichern!");
+
+ }
+
+ }
+
+
+ return (
+
+
+ {toast && (
+ setToast(null)}
+ />
+ )}
+
+
+
+
+
+ {
+ true &&
+
+
{JSON.stringify(watch(), null, 2)}
+
+ }
+
+
+
+ )
+}
diff --git a/src/components/rk2/groups/groupPath.tsx b/src/components/rk2/groups/groupPath.tsx
new file mode 100644
index 0000000..7b08b45
--- /dev/null
+++ b/src/components/rk2/groups/groupPath.tsx
@@ -0,0 +1,22 @@
+"use client"
+
+import { useGroups } from "@/hooks/rk2/useGroups";
+import { GroupResponse } from "@/types/anlagen/groups";
+
+interface Props {
+ groups: GroupResponse[],
+ groupId?: string,
+}
+
+
+export const GroupPath = ({ groups, groupId }: Props) => {
+ const { getPathToRoot } = useGroups({ groups: groups })
+
+ return (
+ <>
+ {groupId ?
+ {getPathToRoot(groupId).map((g)=>g.name).join(".")}
:
+ }
+ >
+ )
+}
diff --git a/src/components/rk2/groups/groupSelect.tsx b/src/components/rk2/groups/groupSelect.tsx
new file mode 100644
index 0000000..974b006
--- /dev/null
+++ b/src/components/rk2/groups/groupSelect.tsx
@@ -0,0 +1,44 @@
+"use client"
+import { useGroups } from "@/hooks/rk2/useGroups"
+import { useGroupTree } from "@/hooks/rk2/useGroupTree"
+import { GroupResponse } from "@/types/anlagen/groups"
+import { GroupTreeView } from "./groupTreeView"
+import { useEffect, useState } from "react"
+
+
+
+
+interface Props {
+ groups: GroupResponse[]
+ onSelect: (group: GroupResponse) => void
+}
+
+export function GroupSelect({ groups, onSelect }: Props) {
+
+ const { getGroupById, groupsExtended } = useGroups({ groups })
+ const { tree, close, closeAll, isOpen, open, openAll, openGroups, toggle, isSelected, select, selectedGroupId } = useGroupTree(groups);
+ const [modalClose,setModalClose] = useState(false);
+
+ useEffect(() => {
+ if (selectedGroupId) {
+ const group = getGroupById(selectedGroupId);
+ if (group){
+ onSelect(group)
+ setModalClose(true)
+ }
+ }
+ }, [selectedGroupId]);
+
+ const handelModalClose = ()=>{
+ setModalClose(false);
+ }
+
+ return (
+
+
+
+
+
+ )
+
+}
diff --git a/src/components/rk2/groups/groupTreeView.tsx b/src/components/rk2/groups/groupTreeView.tsx
new file mode 100644
index 0000000..be56035
--- /dev/null
+++ b/src/components/rk2/groups/groupTreeView.tsx
@@ -0,0 +1,56 @@
+"use client"
+import { GroupTree } from "@/types/anlagen/groups"
+
+interface GroupTreeViewProps {
+ nodes: GroupTree[]
+ isOpen: (id: string) => boolean
+ toggle: (id: string) => void
+ isSelected: (id: string) => boolean
+ select: (id: string|undefined) => void
+}
+
+export function GroupTreeView({ nodes, isOpen, toggle, select, isSelected }: GroupTreeViewProps) {
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/rk2/groups/groupViewItem.tsx b/src/components/rk2/groups/groupViewItem.tsx
new file mode 100644
index 0000000..a0db949
--- /dev/null
+++ b/src/components/rk2/groups/groupViewItem.tsx
@@ -0,0 +1,47 @@
+"use client"
+
+import { Sites } from "@/_config/sites"
+import { api_groups_delete } from "@/actions/rk2/api-groups"
+
+import { Button } from "@/components/ui/button"
+import { ListItem, ListItemAction } from "@/components/ui/list"
+import { GroupResponse } from "@/types/anlagen/groups"
+import { useRouter } from "next/navigation"
+
+
+
+
+interface Props {
+ group: GroupResponse,
+ parent?: GroupResponse
+}
+
+
+
+export function GroupViewItem({ group, parent }: Props) {
+
+ let router = useRouter()
+
+ return (
+
+ {parent ?
+ {parent.name}
+
: root
}
+
+
+ {group.name}
+
+
+ {group.nr}
+
+
+
+
+
+
+
+
+
+ )
+
+}
diff --git a/src/components/rk2/groups/groupViewList.tsx b/src/components/rk2/groups/groupViewList.tsx
new file mode 100644
index 0000000..409f68d
--- /dev/null
+++ b/src/components/rk2/groups/groupViewList.tsx
@@ -0,0 +1,69 @@
+"use client"
+import { useGroups } from "@/hooks/rk2/useGroups"
+import { useGroupTree } from "@/hooks/rk2/useGroupTree"
+import { GroupResponse } from "@/types/anlagen/groups"
+import { GroupTreeView } from "./groupTreeView"
+import { GroupViewItem } from "./groupViewItem"
+import { GroupForm } from "./groupForm"
+import { Anlage } from "@/types/rk"
+import Modal from "@/components/ui/Modal"
+import { GroupSelect } from "./groupSelect"
+import { GroupEdit } from "./groupEdit"
+import MonButton from "@/components/ui/MonBotton"
+import Link from "next/link"
+import { Sites } from "@/_config/sites"
+import { List, ListItem } from "@/components/ui/list"
+
+
+
+
+interface Props {
+ anlage: Anlage,
+ groups: GroupResponse[]
+}
+
+export function GroupViewList({ anlage, groups }: Props) {
+
+ const { getGroupById, groupsExtended } = useGroups({ groups })
+ const { tree, close, closeAll, isOpen, open, openAll, openGroups, toggle, isSelected, select, selectedGroupId } = useGroupTree(groups);
+
+
+ return (
+
+
+ {/* Gruppe neu anlegen */}
+
+ {Sites.rk2.groups.new.name}
+
+
+
+ {/* Grupplist flat */}
+
+
+ {groupsExtended.map((item) =>
+
+
+
+
+ )}
+
+
+
+
+ {/* Grupplist as Tree */}
+
+
+
select(undefined)}>root
+
+
+
+ {selectedGroupId &&
+
+
+ }
+
+
+
+ )
+
+}
diff --git a/src/components/rk2/signals/signalForm.tsx b/src/components/rk2/signals/signalForm.tsx
new file mode 100644
index 0000000..29b39ec
--- /dev/null
+++ b/src/components/rk2/signals/signalForm.tsx
@@ -0,0 +1,81 @@
+"use client"
+
+import { api_signals, api_signals_create } from "@/actions/rk2/api-signals";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Select } from "@/components/ui/select";
+import { useSignal } from "@/hooks/rk2/useSignal";
+import { ComponentResponse } from "@/types/anlagen/components"
+import { SignalCreate, SignalResponse, SignalType } from "@/types/anlagen/signals"
+import { useEffect } from "react";
+
+
+
+interface Props {
+ componente: ComponentResponse,
+ initSignal?: SignalResponse,
+}
+
+export function SignalForm({ componente, initSignal }: Props) {
+
+ const { setSignal, setValue, signalData, string_to_signalType } = useSignal({});
+
+/*
+ useEffect(() => {
+ if (initSignal) setSignal({...initSignal,componentId:componente.id});
+ }, [initSignal]);
+*/
+ useEffect(() => {
+ setValue("componentId",componente.id);
+ //if (initSignal) setSignal({...initSignal,componentId:componente.id});
+ // setSignal({componentId:componente.id});
+ }, [componente]);
+
+ const handlerSave = ()=>{
+ console.log("save")
+ const saveData:SignalCreate ={
+ componentId:signalData.componentId!,
+ name:signalData.name!,
+ description:signalData.description!,
+ type:signalData.type!,
+ }
+ api_signals_create(saveData);
+ }
+
+ return (
+
+
+ {/*---Form Data */}
+
+
+
+ setValue('name', e.target.value)} />
+
+
+
+
+
+
+
+ setValue('description', e.target.value)} />
+
+
+
+
+
+ {true &&
+
+
{JSON.stringify(signalData, null, 2)}
+
+
+
+ }
+
+ )
+}
diff --git a/src/components/rk2/signals/signalItem.tsx b/src/components/rk2/signals/signalItem.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/rk2/signals/signalList.tsx b/src/components/rk2/signals/signalList.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/rk2/units/componentForm.tsx b/src/components/rk2/units/componentForm.tsx
new file mode 100644
index 0000000..92a6421
--- /dev/null
+++ b/src/components/rk2/units/componentForm.tsx
@@ -0,0 +1,132 @@
+"use client"
+
+import { PropertyField } from "./propertyFields"
+import { ComponentDefaults, ComponentResponse, ComponentType, ComponentTypeDefinition } from "@/types/anlagen/components"
+import { Input } from "@/components/ui/input"
+import { useComponent } from "@/hooks/rk2/useComponent"
+import { Button } from "@/components/ui/button"
+import { Label } from "@/components/ui/label"
+import { Select } from "@/components/ui/select"
+import { useSignal } from "@/hooks/rk2/useSignal"
+import { Card, CardContent, CardHeader } from "@/components/ui/card"
+
+
+
+interface Props {
+ component?: ComponentResponse
+ componentTypes: ComponentTypeDefinition[]
+ componentDefaults: ComponentDefaults[]
+ onClose?: () => void
+}
+
+export function ComponentForm({ component, componentTypes, componentDefaults, onClose }: Props) {
+
+
+ const { findCompType,
+ findCompPropertyType,
+ updateComponent,
+ compnentData,
+ setValue,
+ changePropertie,
+ setDefaultPropertie
+ } = useComponent({ component, componentTypes, componentDefaults });
+
+
+ const { signalByComp } = useSignal({ componenteId: component?.id })
+
+ const handleSaveChanges = async ()=>{
+ await updateComponent(compnentData.id, compnentData)
+ if(onClose) onClose();
+ }
+
+ if (!component) return <>{true &&
+
+ Debug
+
+ {JSON.stringify(compnentData, null, 2)}
+
+
+
+ }>
+ return (
+
+
+ {/*--- Component Basisdaten */}
+
+
+
+
+ {/*--- Properties */}
+
+
Eigenschaften
+
+ {compnentData.properties &&
+ Object.entries(compnentData.properties).map(([key, value]) => (
+
+
changePropertie(name, value)}
+ />
+ ))}
+
+
+
+
+
+
+
+
+ {/* --- Signal IDs ----*/}
+ {signalByComp && signalByComp.map((s) =>
+ {s.name}
+
)}
+ {true &&
+
+ Debug
+
+ {JSON.stringify(compnentData, null, 2)}
+
+
+
+ }
+
+ )
+}
+/*
+
+*/
\ No newline at end of file
diff --git a/src/components/rk2/units/propertyFields.tsx b/src/components/rk2/units/propertyFields.tsx
new file mode 100644
index 0000000..790d9e2
--- /dev/null
+++ b/src/components/rk2/units/propertyFields.tsx
@@ -0,0 +1,78 @@
+import { Input } from "@/components/ui/input"
+import { PropertyDefinition } from "@/types/anlagen/components"
+
+
+interface PropertyFieldProps {
+ name: string
+ propertyType?: PropertyDefinition,
+ value: unknown,
+ setPropValue: (name:string,value:unknown)=>void
+
+}
+
+export function PropertyField({ name, value, propertyType,setPropValue }: PropertyFieldProps) {
+
+
+ if (!propertyType) return
+ {name} {value as string} {propertyType}
+
+
+ const handleChangeValue = (value:unknown,type:string)=>{
+
+ setPropValue(name,value);
+ }
+
+ switch (propertyType.type) {
+
+ case "text":
+ return (
+
+
+ handleChangeValue(e.target.value,"text")} />
+
+ )
+
+ case "number":
+ return (
+
+
+ handleChangeValue(e.target.value,"number")} />
+
+
+ )
+
+ case "boolean":
+ return (
+
+
+ handleChangeValue(e.target.checked,"boolean")} />
+
+ )
+
+ case "select":
+ return (
+
+
+
+
+
+ )
+
+ default:
+ return null
+ }
+}
\ No newline at end of file
diff --git a/src/components/rk2/units/unitEdit.tsx b/src/components/rk2/units/unitEdit.tsx
new file mode 100644
index 0000000..596e86c
--- /dev/null
+++ b/src/components/rk2/units/unitEdit.tsx
@@ -0,0 +1,70 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import Modal from "@/components/ui/Modal";
+import { Anlage } from "@/types/rk";
+import { GroupResponse } from "@/types/anlagen/groups";
+import { UnitResponse } from "@/types/anlagen/units";
+import { useGroups } from "@/hooks/rk2/useGroups";
+import { UnitCreateForm } from "./unit_createForm";
+import { GroupSelect } from "../groups/groupSelect";
+import { ComponentDefaults, ComponentTypeDefinition } from "@/types/anlagen/components";
+
+
+
+
+
+interface Params {
+ anlage: Anlage,
+ groups: GroupResponse[],
+ selectedUnit?: UnitResponse,
+ componentTypes: ComponentTypeDefinition[]
+ componentDefaults: ComponentDefaults[]
+}
+
+export const UnitEdit = ({ anlage, groups, selectedUnit, componentTypes,componentDefaults }: Params) => {
+
+
+ const { getGroupById } = useGroups({ groups });
+ const [showSelectGroup, setShowSelectGroup] = useState(false);
+ const [selectedGroup, setSelectedGroup] = useState();
+
+
+ useEffect(() => {
+ if (selectedUnit && selectedUnit.groupId) {
+ const pGroup = getGroupById(selectedUnit.groupId)
+ setSelectedGroup(() => pGroup)
+ }
+ }, [selectedUnit])
+
+ useEffect(() => {
+ if (selectedGroup) {
+ const pGroup = getGroupById(selectedGroup.id)
+ setSelectedGroup(() => pGroup)
+ }
+ }, [selectedGroup])
+
+
+ const handlerGroupSelect = (group: GroupResponse) => {
+ setSelectedGroup(() => group);
+ setShowSelectGroup(false);
+ }
+
+ //groupIntial={selectedGroup} parentGroup={selectedGroup} onResetParent={() => setSelectedGroup(() => undefined)}
+
+ return (
+
+
+ setShowSelectGroup(true)} unitIntial={selectedUnit} componentTypes={componentTypes} componentDefaults={componentDefaults}/>
+
+ {showSelectGroup &&
+ setShowSelectGroup(false)} >
+
+
+ }
+
+
+
+
+ )
+}
diff --git a/src/components/rk2/units/unit_createForm.tsx b/src/components/rk2/units/unit_createForm.tsx
new file mode 100644
index 0000000..c339c49
--- /dev/null
+++ b/src/components/rk2/units/unit_createForm.tsx
@@ -0,0 +1,280 @@
+"use client";
+
+import { useEffect, useState } from "react"
+import { Anlage } from "@/types/rk";
+import Toast from "@/components/ui/Toast";
+import { UnitResponse } from "@/types/anlagen/units";
+import { useUnit } from "@/hooks/rk2/useUnit";
+import { api_units_create, api_units_update } from "@/actions/rk2/api-units";
+import { ComponentForm } from "./componentForm";
+import { GroupResponse } from "@/types/anlagen/groups";
+import { Button } from "@/components/ui/button";
+import { ComponentDefaults, ComponentResponse, ComponentTypeDefinition } from "@/types/anlagen/components";
+import { useComponent } from "@/hooks/rk2/useComponent";
+import { ComponentIcon, Divide, Save, TableOfContents } from "lucide-react";
+import { Card, CardContent, CardFooder, CardHeader, CardTitle } from "@/components/ui/card";
+import { List, ListItem, ListItemAction, ListItemContent, ListItemDescription, ListItemIcon, ListItemTitle } from "@/components/ui/list";
+import { Label } from "@/components/ui/label";
+import { Input } from "@/components/ui/input";
+import { Select } from "@/components/ui/select";
+
+
+
+interface Params {
+ anlage: Anlage,
+ unitIntial?: UnitResponse
+ selectedGroup?: GroupResponse
+ onDisplaySelectGroup: () => void
+ componentTypes: ComponentTypeDefinition[]
+ componentDefaults: ComponentDefaults[]
+ debug?: boolean
+}
+
+export const UnitCreateForm = ({ anlage, unitIntial, onDisplaySelectGroup, selectedGroup, componentTypes, componentDefaults, debug }: Params) => {
+ const [id, setId] = useState();
+ const [selectedComponent, setSelectedComponent] = useState();
+
+
+ const { emptyUnit, unitType, setValue, unitData, string_to_UnitType } = useUnit({ unit: unitIntial });
+ const { componentNames, findCompType, createComponent, removeComponent } = useComponent({ componentTypes, componentDefaults });
+
+ const [toast, setToast] = useState(null);
+
+
+ useEffect(() => {
+ console.log('init');
+ if (unitIntial) {
+ const { id, ...rest } = unitIntial;
+ setId(id);
+ } else {
+ }
+ }, [unitIntial]);
+
+ //--- Parent Group hat sich geändert
+ useEffect(() => {
+ selectedGroup && setValue("groupId", selectedGroup.id);
+ }, [selectedGroup])
+
+
+ //--- Submit --> Create oder Update
+ const onSubmit = async () => {
+
+ const { id: id1, ...sendData } = unitData;
+
+ let response = id ? await api_units_update(id, sendData) : await api_units_create(sendData);
+
+ if (response.success) {
+ setId(response.data!.id);
+ setToast("Erfolgreich gespeichert!");
+ } else {
+ setToast("Fehler beim Speichern!");
+
+ }
+
+ }
+
+ const handlerAppendComponent = (type: string) => {
+ const comp = findCompType(type);
+ if (!comp || !id) return;
+ console.log(comp)
+ createComponent(id, { name: "", type: comp.type })
+ }
+
+
+ return (
+ <>
+
+ {toast && (
+ setToast(null)}
+ />
+ )}
+
+
+ Unit
+
+
+
+
+
+
+
{selectedGroup ? selectedGroup.name : "noGroup"}
+
+
+
+
+
+
+ setValue('prefix', e.target.value)} />
+
+
+
+
+
+ setValue('bmk', e.target.value)} />
+
+
+
+
+
+ setValue('name', e.target.value)} />
+
+
+
+
+ setValue('postfix', e.target.value)} />
+
+
+
+
+
+
+
+
+
+
+
+
+ setValue('info', e.target.value)} />
+
+
+
+
+
+
+ {selectedComponent &&
+
+
+ }
+
+
+ {/* Componente */}
+
+
+ {/*--- Show addable Components */}
+ {
+ !selectedComponent &&
+ {id &&
+
+ Komponenten +
+
+ {componentNames.map((comp) => (
+
+
+
+ ))}
+
+
+ }
+
+
+ {/*--- Show Components List */}
+ {id && unitData.components &&
+
+ Komponenten
+
+
+
+ {unitData.components.map((c, index) =>
+
+
+
+
+
+
+ {c.type}
+
+
+
+ {c.prefix && c.prefix}
+ {c.name}
+ {c.postfix && c.postfix}
+ {c.index && c.index > 0 ? c.index: ""}
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ }
+
+
+ }
+ {/* --- Components */}
+
+
+
+ setSelectedComponent(() => undefined)}
+ />
+
+
+ {
+ true &&
+
+ Debug
+
+ {JSON.stringify(unitData, null, 2)}
+
+
+
+ }
+
+ >
+
+ )
+}
+/*
+ {fields.length === 0 && (Noch keine Components vorhanden.
)}
+
+ {selectedComponent && (
+
+ handlerRemoveItem(index)}
+ setValue={setValue}
+ componentTypes={componentTypes}
+ componentDefaults={componentDefaults}
+
+ />
+
+ )}
+
+ {false && fields.map(
+ (field, index) => (
+ handlerRemoveItem(index)}
+ setValue={setValue}
+ componentTypes={componentTypes}
+ componentDefaults={componentDefaults}
+
+ />
+ )
+ )}
+
+
+*/
\ No newline at end of file
diff --git a/src/components/test_dnd.tsx b/src/components/test_dnd.tsx
new file mode 100644
index 0000000..9df1d4d
--- /dev/null
+++ b/src/components/test_dnd.tsx
@@ -0,0 +1,37 @@
+"use client";
+
+import { DndContext, closestCenter } from "@dnd-kit/core";
+import {
+ arrayMove,
+ SortableContext,
+ useSortable,
+ verticalListSortingStrategy,
+} from "@dnd-kit/sortable";
+import { CSS } from "@dnd-kit/utilities";
+import { useState } from "react";
+
+interface Props{
+ item:{id:string,name:string},
+ children: React.ReactNode
+}
+
+export function SortableItem({ item,children }:Props) {
+ const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id:item.id });
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ padding: "12px",
+ border: "1px solid #ccc",
+ marginBottom: "8px",
+ background: "white",
+ cursor: "grab",
+ };
+
+ return (
+
+ {item.id} {item.name}
+ {children}
+
+ );
+}
diff --git a/src/components/ui/InputNumber.tsx b/src/components/ui/InputNumber.tsx
new file mode 100644
index 0000000..ba0304b
--- /dev/null
+++ b/src/components/ui/InputNumber.tsx
@@ -0,0 +1,78 @@
+"use client";
+
+import { error } from "console";
+import { useEffect, useRef, useState } from "react";
+
+
+interface Props {
+ inValue: number,
+ name: string,
+ onChanged: (name: string, newValue: number) => void
+}
+
+export function InputNumber({ inValue, name, onChanged }: Props) {
+
+ const [value, setValue] = useState('');
+ const [lastValue, setLastValue] = useState(0);
+ const [errorNumber, setErrorNumber] = useState(false);
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ setValue("" + inValue);
+ setLastValue(inValue);
+ }, [inValue])
+
+ const handleValueChange = (newValue: string) => {
+ const isNumber = /^-?\d+$/.test(newValue);
+ if (isNumber) {
+ setValue(newValue);
+
+ setErrorNumber(false);
+ } else {
+ setErrorNumber(true);
+ }
+ }
+
+ const handleSubmit = () => {
+
+ if (!errorNumber && value !== lastValue.toString()) {
+ onChanged(name, Number(value));
+ }
+ else setValue("" + lastValue);
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ console.log(e.key);
+ if (e.key == 'ArrowUp' && !e.ctrlKey) {
+ e.preventDefault();
+ setValue((old) => "" + (Number(old) + 1))
+ }
+
+ if (e.key == 'ArrowUp' && e.ctrlKey) {
+ e.preventDefault();
+ setValue((old) => "" + (Number(old) + 10))
+ }
+
+ if (e.key == 'ArrowDown' && !e.ctrlKey) {
+ e.preventDefault();
+ setValue((old) => "" + (Number(old) - 1))
+ }
+
+ if (e.key == 'ArrowDown' && e.ctrlKey) {
+ e.preventDefault();
+ setValue((old) => "" + (Number(old) - 10))
+ }
+
+ if (e.key == 'Escape') {
+ e.preventDefault();
+ setValue((old) => "" + lastValue)
+ }
+ }
+
+ return (
+ <>
+ handleKeyDown(e)} value={value} onBlur={() => handleSubmit()} onChange={(e) => handleValueChange(e.target.value)} />
+ {errorNumber && Error not a number
}
+ >
+ );
+}
\ No newline at end of file
diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx
new file mode 100644
index 0000000..9a19da9
--- /dev/null
+++ b/src/components/ui/Modal.tsx
@@ -0,0 +1,63 @@
+"use client";
+
+import { useEffect } from "react";
+import { useRouter } from "next/navigation";
+
+type ModalProps = {
+ children: React.ReactNode;
+ goBack?:boolean,
+ doClose?:boolean,
+ onClose?:()=>void
+};
+
+export default function Modal({ children,goBack,onClose,doClose}: ModalProps) {
+ const router = useRouter();
+
+ useEffect(()=>{
+ if(doClose){
+ handlerClose();
+ }
+ },[doClose])
+
+ useEffect(() => {
+ function handleKeyDown(event: KeyboardEvent) {
+ if (event.key === "Escape") {
+ handlerClose()
+ }
+ }
+
+ document.addEventListener("keydown", handleKeyDown);
+
+ return () => {
+ document.removeEventListener("keydown", handleKeyDown);
+ };
+ }, [router]);
+
+ const handlerClose = ()=>{
+ if(goBack) router.back();
+ if(onClose) onClose();
+ }
+
+ return (
+ {
+ if (event.target === event.currentTarget) {
+ handlerClose()
+ }
+ }}
+ >
+
+
+
+ {children}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/ui/MonBotton.tsx b/src/components/ui/MonBotton.tsx
new file mode 100644
index 0000000..df4bb52
--- /dev/null
+++ b/src/components/ui/MonBotton.tsx
@@ -0,0 +1,42 @@
+
+"use Client"
+import { ReactEventHandler, ReactNode } from "react";
+
+
+interface Props {
+ onClick: () => void,
+ children: ReactNode;
+}
+
+export default function MonButton({ onClick, children }: Props) {
+
+ const handlerKey = (e:React.KeyboardEvent)=>{
+ const code = e.key;
+ if(code ==='Enter') onClick();
+ }
+ return (
+ handlerKey(e)}
+ onClick={ (e)=>onClick()}
+ tabIndex={0}
+ >
+
+
+ {children}
+
+
+
+
+
+ )
+}
+
+
diff --git a/src/components/ui/Toast.tsx b/src/components/ui/Toast.tsx
new file mode 100644
index 0000000..7081e06
--- /dev/null
+++ b/src/components/ui/Toast.tsx
@@ -0,0 +1,67 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+type ToastProps = {
+ message: string;
+ duration?: number;
+ position?: "top" | "bottom";
+ onClose: () => void;
+};
+
+export default function Toast({
+ message,
+ duration = 3000,
+ position = "bottom",
+ onClose,
+}: ToastProps) {
+ const [visible, setVisible] = useState(false);
+
+ useEffect(() => {
+ const showTimer = setTimeout(() => {
+ setVisible(true);
+ }, 10);
+
+ const hideTimer = setTimeout(() => {
+ setVisible(false);
+ }, duration);
+
+ const removeTimer = setTimeout(() => {
+ onClose();
+ }, duration + 300);
+
+ return () => {
+ clearTimeout(showTimer);
+ clearTimeout(hideTimer);
+ clearTimeout(removeTimer);
+ };
+ }, [duration, onClose]);
+
+ const positionClasses =
+ position === "top"
+ ? "bottom-full mb-2"
+ : "top-full mt-2";
+
+ return (
+
+ {message}
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
new file mode 100644
index 0000000..961b436
--- /dev/null
+++ b/src/components/ui/button.tsx
@@ -0,0 +1,67 @@
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+import { cn } from "@/lib/utils";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center rounded-md font-medium transition-colors",
+ {
+ variants: {
+ variant: {
+ primary: "bg-blue-600 text-white hover:bg-blue-700",
+ secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300",
+ danger: "bg-red-600 text-white hover:bg-red-700",
+ select: "bg-gray-800 text-gray-300 hover:bg-gray-700",
+ ghost: "hover:bg-gray-100",
+ },
+
+ size: {
+ xs: "h-6 px-3 text-xs",
+ sm: "h-8 px-3 text-sm",
+ md: "h-10 px-4 text-sm",
+ lg: "h-12 px-6 text-base",
+ },
+ },
+
+ defaultVariants: {
+ variant: "primary",
+ size: "md",
+ },
+ }
+);
+
+type ButtonProps =
+ React.ComponentProps<"button"> &
+ VariantProps & {
+ asChild?: boolean;
+ };
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ children,
+ ...props
+}: ButtonProps) {
+ const Comp = asChild ? Slot : "button";
+
+ return (
+
+ {children}
+
+ );
+}
+
+export {
+ Button,
+ buttonVariants,
+};
\ No newline at end of file
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx
new file mode 100644
index 0000000..b775fe2
--- /dev/null
+++ b/src/components/ui/card.tsx
@@ -0,0 +1,158 @@
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+import { cn } from "@/lib/utils";
+
+const cardVariants = cva(
+ "mb-2",//"inline-flex items-center justify-center rounded-md font-medium transition-colors",
+ {
+ variants: {
+ variant: {
+ primary: "border-1 flex flex-col rounded shadow border-gray-200",
+ secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300",
+ danger: "bg-red-600 text-white hover:bg-red-700",
+ select: "bg-gray-800 text-gray-300 hover:bg-gray-700",
+ ghost: "hover:bg-gray-100",
+ },
+
+ positons: {
+ start:"items-start",
+ center:"",
+ end:"items-end"
+ },
+
+ size: {
+ xs: "h-6 px-3 text-xs",
+ sm: "h-8 px-3 text-sm",
+ md: "h-10 px-4 text-sm",
+ lg: "h-12 px-6 text-base",
+ },
+ },
+
+ defaultVariants: {
+ variant: "primary",
+ positons:"center"
+
+ },
+ }
+);
+
+const cardHeaderVariants = cva(
+ "flex items-center justify-center w-full text-gray-800",//"inline-flex items-center justify-center rounded-md font-medium transition-colors",
+ {
+ variants: {
+ variant: {
+ default: "border-b-1 bg-gray-200 border-gray-400 font-bold mb-2",
+ danger: "border-b-1 bg-red-600 border-gray-400 font-bold text-white mb-2",
+ ghost: "border-b-1 w-full border-gray-400 font-bold text-gray-400 mb-2",
+ },
+
+ size: {
+ xs: "h-6 px-3 text-xs",
+ sm: "h-8 px-3 text-sm",
+ md: "h-10 px-4 text-sm",
+ lg: "h-12 px-6 text-base",
+ },
+ },
+
+ defaultVariants: {
+ variant: "default",
+
+ },
+ }
+);
+
+type CardProps =
+ React.ComponentProps<"div"> &
+ VariantProps & {
+ asChild?: boolean;
+ };
+
+
+function Card({ className, variant, size,positons, asChild = false, children, ...props }: CardProps) {
+ const Comp = asChild ? Slot : "div";
+
+ return (
+
+ {children}
+
+ );
+}
+
+/*
+ CardHeader
+*/
+
+type CardHeaderProps =
+ React.ComponentProps<"div"> &
+ VariantProps
+
+function CardHeader({ className, variant, size, ...props }: CardHeaderProps) {
+ return (
+
+ );
+}
+
+/*
+ CardTitle
+*/
+function CardTitle({ className, ...props }: React.ComponentProps<"h3">) {
+ return (
+
+ );
+}
+
+/*
+ CardContent
+*/
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardFooder({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+export {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+ CardFooder,
+ cardVariants,
+};
\ No newline at end of file
diff --git a/src/components/ui/droppableContainer.tsx b/src/components/ui/droppableContainer.tsx
new file mode 100644
index 0000000..e77c688
--- /dev/null
+++ b/src/components/ui/droppableContainer.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import { useDroppable } from "@dnd-kit/core";
+
+interface Props {
+ id: string;
+ children: React.ReactNode;
+}
+
+export function DroppableContainer({ id, children }: Props) {
+ const { setNodeRef } = useDroppable({
+ id,
+ });
+
+ return (
+
+ {children}
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/ui/editable-list/useEditableList.ts b/src/components/ui/editable-list/useEditableList.ts
new file mode 100644
index 0000000..47fbb5f
--- /dev/null
+++ b/src/components/ui/editable-list/useEditableList.ts
@@ -0,0 +1,128 @@
+import { RefObject, useEffect, useState } from "react";
+
+interface Props {
+ ref: RefObject;
+ data: T[];
+
+ onSave: (item: T) => Promise | void;
+
+};
+
+export function useEditableList({ ref, data, onSave }: Props) {
+
+ const [selectedIndex, setSelectedIndex] = useState(0);
+ const [editingIndex, setEditingIndex] = useState(null);
+
+ const save = async (item: T) => {
+
+ await onSave(item);
+ setEditingIndex(null);
+ };
+
+ useEffect(() => {
+ if (editingIndex == null) {
+ console.log('set focus to div')
+
+ ref.current?.focus();
+ }
+ }, [editingIndex])
+
+ const onKeyDown = (e: React.KeyboardEvent) => {
+
+ if (editingIndex !== null) return;
+
+ console.log(e.key)
+ switch (e.key) {
+
+ case "ArrowDown":
+ e.preventDefault();
+
+ setSelectedIndex(
+ Math.min(selectedIndex + 1, data.length - 1)
+ );
+ break;
+
+
+ case "ArrowUp":
+ e.preventDefault();
+
+ setSelectedIndex(
+ Math.max(selectedIndex - 1, 0)
+ );
+ break;
+
+
+ case "Home":
+ e.preventDefault();
+
+ setSelectedIndex(0);
+ break;
+
+
+ case "End":
+ e.preventDefault();
+
+ setSelectedIndex(data.length - 1);
+ break;
+
+ case "+":
+ console.log('Plus')
+ break;
+ case "Enter":
+ e.preventDefault();
+
+ setEditingIndex(selectedIndex);
+ break;
+
+ }
+
+ }
+
+
+ function select(index: number) {
+ setSelectedIndex(index);
+ }
+
+ function startEdit(index = selectedIndex) {
+ setEditingIndex(index);
+ }
+
+ function stopEdit() {
+ setEditingIndex(null);
+ }
+
+ function cancelEdit() {
+
+ setEditingIndex(null);
+
+ }
+
+
+
+
+
+ return {
+ isEditing: editingIndex != null,
+ selectedIndex,
+ editingIndex,
+
+ selectedItem: data[selectedIndex],
+
+ editingItem: editingIndex === null ? undefined : data[editingIndex],
+
+ isItemSelected(index: number) {
+ return index === selectedIndex;
+ },
+ isItemEditing(index: number) {
+ return index === editingIndex;
+ },
+
+ onKeyDown,
+ select,
+ startEdit,
+ stopEdit,
+ cancelEdit,
+ save,
+ setSelectedIndex,
+ }
+}
\ No newline at end of file
diff --git a/src/components/ui/editable-list/useKeyboradNavigation.ts b/src/components/ui/editable-list/useKeyboradNavigation.ts
new file mode 100644
index 0000000..a0adb48
--- /dev/null
+++ b/src/components/ui/editable-list/useKeyboradNavigation.ts
@@ -0,0 +1,97 @@
+import { useEffect } from "react";
+
+type KeyboardNavigationOptions = {
+ enabled?: boolean;
+ count: number;
+ selectedIndex: number;
+ setSelectedIndex: (index: number) => void;
+
+ onEnter?: () => void;
+ onEscape?: () => void;
+};
+
+export function useKeyboardNavigation({
+ enabled = true,
+ count,
+ selectedIndex,
+ setSelectedIndex,
+ onEnter,
+ onEscape,
+}: KeyboardNavigationOptions) {
+
+ useEffect(() => {
+ if (!enabled) return;
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ switch (e.key) {
+
+ case "ArrowDown":
+ e.preventDefault();
+
+ setSelectedIndex(
+ Math.min(selectedIndex + 1, count - 1)
+ );
+ break;
+
+
+ case "ArrowUp":
+ e.preventDefault();
+
+ setSelectedIndex(
+ Math.max(selectedIndex - 1, 0)
+ );
+ break;
+
+
+ case "Home":
+ e.preventDefault();
+
+ setSelectedIndex(0);
+ break;
+
+
+ case "End":
+ e.preventDefault();
+
+ setSelectedIndex(count - 1);
+ break;
+
+
+ case "Enter":
+ e.preventDefault();
+
+ onEnter?.();
+ break;
+
+
+ case "Escape":
+ e.preventDefault();
+
+ onEscape?.();
+ break;
+ }
+ };
+
+
+ window.addEventListener(
+ "keydown",
+ handleKeyDown
+ );
+
+
+ return () => {
+ window.removeEventListener(
+ "keydown",
+ handleKeyDown
+ );
+ };
+
+ }, [
+ enabled,
+ count,
+ selectedIndex,
+ setSelectedIndex,
+ onEnter,
+ onEscape,
+ ]);
+}
\ No newline at end of file
diff --git a/src/components/ui/editable/EditableCell.tsx b/src/components/ui/editable/EditableCell.tsx
new file mode 100644
index 0000000..26727dd
--- /dev/null
+++ b/src/components/ui/editable/EditableCell.tsx
@@ -0,0 +1,14 @@
+import { ReactNode } from "react";
+
+type Props = {
+ editing: boolean;
+ display: ReactNode;
+ editor: ReactNode;
+};
+
+export function EditableCell({ editing, display, editor }: Props) {
+
+ return editing
+ ? editor
+ : {display}
;
+}
\ No newline at end of file
diff --git a/src/components/ui/editable/EditableRow.tsx b/src/components/ui/editable/EditableRow.tsx
new file mode 100644
index 0000000..61bee2c
--- /dev/null
+++ b/src/components/ui/editable/EditableRow.tsx
@@ -0,0 +1,34 @@
+import clsx from "clsx";
+import { ReactNode } from "react";
+
+type Props = {
+ children: ReactNode;
+ selected?: boolean;
+ columns?: string;
+
+};
+
+export function EditableRow({ children, selected = false, columns }: Props) {
+
+
+ return (
+
+ {children}
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/ui/editable/NumberCell.tsx b/src/components/ui/editable/NumberCell.tsx
new file mode 100644
index 0000000..be483a9
--- /dev/null
+++ b/src/components/ui/editable/NumberCell.tsx
@@ -0,0 +1,32 @@
+import { forwardRef } from "react";
+import { EditableCell } from "./EditableCell";
+
+type Props = {
+ value: string;
+ editing: boolean;
+
+ onChange(value: string): void;
+ onKey?: React.KeyboardEventHandler;
+};
+
+export const NumberCell = forwardRef(({ value, editing, onChange,onKey }, ref) => {
+ return (
+ onChange(e.target.value)}
+ />
+ }
+
+ />
+ );
+})
+
+
diff --git a/src/components/ui/editable/TextCell.tsx b/src/components/ui/editable/TextCell.tsx
new file mode 100644
index 0000000..71bd0b5
--- /dev/null
+++ b/src/components/ui/editable/TextCell.tsx
@@ -0,0 +1,33 @@
+import { forwardRef } from "react";
+import { EditableCell } from "./EditableCell";
+
+type Props = {
+ value: string;
+ editing: boolean;
+
+ onChange(value: string): void;
+ onKey?:React.KeyboardEventHandler;
+};
+
+export const TextCell = forwardRef(({ value, editing, onChange, onKey }, ref) => {
+
+ return (
+ }
+ editor={
+ onChange(e.target.value)}
+ />
+ }
+
+ />
+ );
+
+}
+);
\ No newline at end of file
diff --git a/src/components/ui/editable/useFocusManager.ts b/src/components/ui/editable/useFocusManager.ts
new file mode 100644
index 0000000..9b1b132
--- /dev/null
+++ b/src/components/ui/editable/useFocusManager.ts
@@ -0,0 +1,47 @@
+import { useRef } from "react";
+
+export function useFocusManager() {
+
+ const elements = useRef<(HTMLElement | null)[]>([]);
+
+ const register = (index: number) => (element: HTMLElement | null) => {
+
+ elements.current[index] = element;
+ };
+
+
+ const focusFirst = () => {
+ elements.current[0]?.focus();
+ };
+
+ const focusNext = (current: HTMLElement) => {
+
+ const index = elements.current.indexOf(current);
+
+ if (index === -1) return;
+
+ const next = (index + 1) % elements.current.length;
+
+ elements.current[next]?.focus();
+ };
+
+ const focusPrevious = (current: HTMLElement) => {
+
+ const index = elements.current.indexOf(current);
+
+ if (index === -1) return;
+
+ const previous = (index - 1 + elements.current.length) % elements.current.length;
+
+ elements.current[previous]?.focus();
+ };
+
+
+
+ return {
+ register,
+ focusFirst,
+ focusNext,
+ focusPrevious,
+ };
+}
\ No newline at end of file
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
new file mode 100644
index 0000000..77b681f
--- /dev/null
+++ b/src/components/ui/input.tsx
@@ -0,0 +1,73 @@
+import {
+ cva,
+ type VariantProps,
+} from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const inputVariants = cva(
+ [
+ "w-full",
+ "rounded-md",
+ "border",
+ "outline-none",
+ "transition-colors",
+ "placeholder:text-gray-400",
+ "focus:ring-2",
+ ],
+ {
+ variants: {
+ variant: {
+ default:
+ "border-gray-300 focus:border-blue-500 focus:ring-blue-500/20",
+
+ error:
+ "border-red-500 focus:border-red-500 focus:ring-red-500/20",
+
+ success:
+ "border-green-500 focus:border-green-500 focus:ring-green-500/20",
+ },
+
+ sizeVariant: {
+ xs: "h-5 px-3 text-xs",
+ sm: "h-8 px-3 text-sm",
+ md: "h-10 px-3 text-sm",
+ lg: "h-12 px-4 text-base",
+ },
+ },
+
+ defaultVariants: {
+ variant: "default",
+ sizeVariant: "xs",
+ },
+ }
+);
+
+
+
+type InputVariants = VariantProps;
+
+type InputProps =
+ React.ComponentProps<"input"> &
+ InputVariants;
+
+function Input({ className, variant, sizeVariant, ...props }: InputProps) {
+
+ return (
+
+ );
+}
+
+export {
+ Input,
+ inputVariants,
+};
\ No newline at end of file
diff --git a/src/components/ui/inputCheckboxR.tsx b/src/components/ui/inputCheckboxR.tsx
new file mode 100644
index 0000000..dc6a431
--- /dev/null
+++ b/src/components/ui/inputCheckboxR.tsx
@@ -0,0 +1,27 @@
+import { useEffect, useState } from "react"
+import { forwardRef } from "react"
+interface ChangeValue {
+ [key: string]: boolean
+}
+
+interface Props extends React.InputHTMLAttributes {
+ label?: string
+ error?: string | undefined
+
+}
+
+export const InputCheckboxR = forwardRef(
+ ({ label, error, ...props }, ref) => {
+
+
+
+ return (
+
+
+
+ {label &&
{label}
}
+
+
+ )
+ }
+)
\ No newline at end of file
diff --git a/src/components/ui/inputTextR.tsx b/src/components/ui/inputTextR.tsx
new file mode 100644
index 0000000..823fca2
--- /dev/null
+++ b/src/components/ui/inputTextR.tsx
@@ -0,0 +1,30 @@
+import { forwardRef } from "react"
+
+interface InputTextProps
+ extends React.InputHTMLAttributes {
+ label?: string
+ error?: string | undefined
+}
+
+export const InputTextR = forwardRef(
+ ({ label, error, ...props }, ref) => {
+ return (
+
+ {label && (
+
+ )}
+
+
+
+ {false && error &&
{error}
}
+
+ )
+ }
+)
+
+InputTextR.displayName = "InputR"
\ No newline at end of file
diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx
new file mode 100644
index 0000000..f8ba835
--- /dev/null
+++ b/src/components/ui/label.tsx
@@ -0,0 +1,62 @@
+import {
+ cva,
+ type VariantProps,
+} from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const labelVariants = cva(
+ [
+ "w-full",
+ "outline-none",
+ ],
+ {
+ variants: {
+ variant: {
+ default:
+ "flex items-center justify-start font-normal text-gray-600 ",
+
+
+ },
+
+ size: {
+ xs: "text-xs",
+ sm: "text-sm",
+ md: "text-sm",
+ lg: "text-base",
+ },
+ },
+
+ defaultVariants: {
+ variant: "default",
+ size: "xs",
+ },
+ }
+);
+
+
+
+type LabelVariants = VariantProps;
+
+type LabelProps = React.ComponentProps<"label"> & LabelVariants;
+
+function Label({ className, variant, size, ...props }: LabelProps) {
+
+ return (
+
+ );
+}
+
+export {
+ Label,
+ labelVariants,
+};
\ No newline at end of file
diff --git a/src/components/ui/list.tsx b/src/components/ui/list.tsx
new file mode 100644
index 0000000..65e799b
--- /dev/null
+++ b/src/components/ui/list.tsx
@@ -0,0 +1,247 @@
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+import { cn } from "@/lib/utils";
+
+const listVariants = cva(
+ "w-full overflow-hidden transition-colors",//"inline-flex items-center justify-center rounded-md font-medium transition-colors",
+ {
+ variants: {
+ variant: {
+ transparent: "",
+ default: "border-gray-200",
+ muted: "border-gray-200 bg-gray-50",
+ dark: "border-gray-700 bg-gray-900 text-white",
+ },
+
+ border: {
+ none: "",
+ primary: "border-1 border-gray-800",
+ secondary: "border-1 border-gray-200",
+ danger: "border-1 border-red-600",
+ },
+
+ size: {
+ xs: "px-3 text-xs",
+ sm: "px-3 text-sm",
+ md: "px-3 text-sm",
+ lg: "px-3 text-base",
+ },
+
+ divide: {
+ true: "divide-y divide-gray-300",
+ false: "",
+ },
+
+ },
+
+ defaultVariants: {
+ variant: "transparent",
+ size: "xs",
+ divide: true,
+ border: "none"
+
+ },
+ }
+);
+
+const listItemVariants = cva(
+ "flex items-center gap-4 transition-colors",//"flex items-center justify-between transition-colors",
+ {
+ variants: {
+
+
+
+ selected: {
+ true: "bg-blue-50 text-blue-700",
+ false: "hover:bg-gray-50",
+ },
+
+ size: {
+ sm: "px-3 py-1",
+ md: "px-3 py-1",
+ lg: "px-3 py-1",
+ },
+
+ hover: {
+ true: "hover:bg-gray-100",
+ false: "",
+ },
+ },
+
+ defaultVariants: {
+ size: "md",
+ hover: false,
+
+ },
+ }
+);
+
+/* ----------------------------------------------------------------------------
+ List
+ ----------------------------------------------------------------------------*/
+type ListProps =
+ React.ComponentProps<"ul"> &
+ VariantProps;
+
+
+function List({ className, variant, size, border, divide, children, ...props }: ListProps) {
+ const Comp = "ul";
+
+ return (
+
+ {children}
+
+ );
+}
+
+/* ----------------------------------------------------------------------------
+ ListHeader
+----------------------------------------------------------------------------*/
+type ListHeaderProps =
+ React.ComponentProps<"div"> ;
+
+function ListHeader({ className,children, ...props }: ListHeaderProps) {
+ const Comp = "div";
+
+ return (
+
+ {children}
+
+ );
+}
+
+/* ----------------------------------------------------------------------------
+ ListHeaderItem
+----------------------------------------------------------------------------*/
+type ListHeaderItemProps =
+ React.ComponentProps<"div"> ;
+
+function ListHeaderItem({ className,children, ...props }: ListHeaderItemProps) {
+ const Comp = "div";
+
+ return (
+
+ {children}
+
+ );
+}
+
+/* ----------------------------------------------------------------------------
+ListItem
+----------------------------------------------------------------------------*/
+type ListItemProps =
+ React.ComponentProps<"li"> &
+ VariantProps;
+
+function ListItem({ className, hover, size, children, ...props }: ListItemProps) {
+ const Comp = "li";
+
+ return (
+
+ {children}
+
+ );
+}
+
+
+/* ----------------------------------------------------------------------------
+ ListIcon
+ ----------------------------------------------------------------------------*/
+function ListItemIcon({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function ListItemContent({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function ListItemTitle({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function ListItemDescription({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function ListItemAction({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+export {
+ List,
+ ListHeader,
+ ListHeaderItem,
+ ListItem,
+ listVariants,
+ ListItemIcon,
+ ListItemTitle,
+ ListItemAction,
+ ListItemContent,
+ ListItemDescription
+};
\ No newline at end of file
diff --git a/src/components/ui/navigation/breadcrumbs.tsx b/src/components/ui/navigation/breadcrumbs.tsx
new file mode 100644
index 0000000..a21698b
--- /dev/null
+++ b/src/components/ui/navigation/breadcrumbs.tsx
@@ -0,0 +1,58 @@
+"use client";
+
+import { ChevronRight } from "lucide-react";
+import { useRouter } from "next/navigation";
+
+export type BreadcrumbItem = {
+ label: string;
+ href?: string;
+};
+
+type BreadcrumbsProps = {
+ items: BreadcrumbItem[];
+};
+
+export default function Breadcrumbs({ items}: BreadcrumbsProps) {
+ const router = useRouter();
+
+ return (
+
+ );
+}
diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx
new file mode 100644
index 0000000..b509d54
--- /dev/null
+++ b/src/components/ui/select.tsx
@@ -0,0 +1,73 @@
+import {
+ cva,
+ type VariantProps,
+} from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const selectVariants = cva(
+ [
+ "w-full",
+ "rounded-md",
+ "border",
+ "outline-none",
+ "transition-colors",
+ "placeholder:text-gray-400",
+ "focus:ring-2",
+ ],
+ {
+ variants: {
+ variant: {
+ default:
+ "border-gray-300 focus:border-blue-500 focus:ring-blue-500/20",
+
+ error:
+ "border-red-500 focus:border-red-500 focus:ring-red-500/20",
+
+ success:
+ "border-green-500 focus:border-green-500 focus:ring-green-500/20",
+ },
+
+ sizeVariant: {
+ xs: "h-5 px-3 text-xs",
+ sm: "h-8 px-3 text-sm",
+ md: "h-10 px-3 text-sm",
+ lg: "h-12 px-4 text-base",
+ },
+ },
+
+ defaultVariants: {
+ variant: "default",
+ sizeVariant: "xs",
+ },
+ }
+);
+
+
+
+type SelectVariants = VariantProps;
+
+type SelectProps =
+ React.ComponentProps<"select"> &
+ SelectVariants;
+
+function Select({ className, variant, sizeVariant, ...props }: SelectProps) {
+
+ return (
+
+ );
+}
+
+export {
+ Select,
+ selectVariants,
+};
\ No newline at end of file
diff --git a/src/components/ui/selectR.tsx b/src/components/ui/selectR.tsx
new file mode 100644
index 0000000..3be2eed
--- /dev/null
+++ b/src/components/ui/selectR.tsx
@@ -0,0 +1,38 @@
+import { forwardRef } from "react"
+
+interface SelectOptions{
+ id:string,
+ value:string
+}
+
+interface SelectProps
+ extends React.InputHTMLAttributes {
+ options:SelectOptions[]
+ label?: string
+ error?: string | undefined
+}
+
+export const SelectR = forwardRef(
+ ({ label, error,options, ...props }, ref) => {
+ return (
+
+ {label && (
+
+ )}
+
+
+
+ {false && error &&
{error}
}
+
+ )
+ }
+)
+
+SelectR.displayName = "SelectR"
\ No newline at end of file
diff --git a/src/components/ui/tableView.tsx b/src/components/ui/tableView.tsx
new file mode 100644
index 0000000..4503c9f
--- /dev/null
+++ b/src/components/ui/tableView.tsx
@@ -0,0 +1,25 @@
+import {JSX} from "react";
+
+interface ColDef{
+ width:number
+ widthTyp:"fr" | "px"
+}
+
+interface Props{
+ cols:ColDef[],
+ children: string | JSX.Element | JSX.Element[]
+}
+
+export default async function TableView({children,cols}:Props) {
+
+ const c0 = cols.reduce((prev,cur)=>`${prev}_${cur.width.toString()}${cur.widthTyp}`,'[__').replace('___','');
+ console.log(c0)
+ const c = `grid grid-cols-${c0+']'} border-b-1 min-h-16 border-gray-200 ps-4 pb-4 pt-2`
+ return (
+
+
+ {children}
+
+ );
+}
+
diff --git a/src/components/visu/create/visuCreatePlatform.tsx b/src/components/visu/create/visuCreatePlatform.tsx
new file mode 100644
index 0000000..5bc7464
--- /dev/null
+++ b/src/components/visu/create/visuCreatePlatform.tsx
@@ -0,0 +1,82 @@
+"use client"
+
+import { useEffect, useRef, useState } from "react";
+import VisuPoint from "./visuPoints";
+import VisuDrawPath from "./visuDrawPath";
+
+
+interface Props {
+}
+
+type Pos = { x: number; y: number };
+type Point = { id: number, x: number; y: number };
+
+export default function VisuCreatePlatform({ }: Props) {
+ const x = 0;
+ const y = 0;
+
+ const [scale, setScale] = useState(1.0);
+ const [points, setPoints] = useState([{ id: 1, x: 50, y: 50 }, { id: 2, x: 100, y: 100 }])
+ const [pos, setPos] = useState({ x: 0, y: 0 });
+ const frame = useRef(null);
+ const latest = useRef({ x: 0, y: 0 });
+
+ const handleMouseMove = (e: React.MouseEvent) => {
+ const rect = e.currentTarget.getBoundingClientRect();
+
+ latest.current = {
+ x: e.clientX - rect.left,
+ y: e.clientY - rect.top,
+ };
+
+ if (frame.current === null) {
+ frame.current = requestAnimationFrame(() => {
+ setPos(latest.current);
+ frame.current = null;
+ });
+ }
+ };
+
+ const handleSelect = (id: number) => {
+ console.log(id)
+ }
+
+ return (
+
+
+
Mausposition
+
X: {pos.x}
+
Y: {pos.y}
+
+
handleMouseMove(e)} >
+
+
+
+
+ );
+}
+
+/*
+
+
+
+ {points.map((point)=>(
+ handleSelect(id)} />
+
+ ))}
+
+
+*/
\ No newline at end of file
diff --git a/src/components/visu/create/visuDrawPath.tsx b/src/components/visu/create/visuDrawPath.tsx
new file mode 100644
index 0000000..5097b61
--- /dev/null
+++ b/src/components/visu/create/visuDrawPath.tsx
@@ -0,0 +1,150 @@
+"use client"
+
+import { PathCommand } from "@/types/draw/draw.path.types";
+import { useEffect, useRef, useState } from "react";
+
+type Pos = { x: number; y: number };
+
+interface Props {
+ id?: number
+ pos?: Pos,
+ onSelect?: (id: number) => void
+}
+
+type Position = { x: number, y: number, data: PathCommand };
+
+export default function VisuDrawPath({ id, pos, onSelect }: Props) {
+
+ const [hasFocus, setHasFocus] = useState(false);
+ const [path, setPath] = useState([])
+ const [positions, setPositions] = useState([])
+ const handleMouseLeafe = () => {
+ console.log('Leaf')
+ setHasFocus(() => false);
+ }
+
+ const handleMouseEnter = () => {
+ console.log('Enter')
+ setHasFocus(() => true);
+ }
+ const raster = 4.0;
+ const scale = 2.0;
+ const l = 9;
+ const h = 7;
+ const y1 = 2 * raster * scale;
+ const x1 = 5 * raster * scale;
+ const x2 = l * raster * scale;
+ const y2 = 4 * raster * scale;
+
+ let l1 = l * raster * scale;
+ let h1 = (h * raster * scale);
+
+ let r = 6 * raster
+
+ useEffect(() => {
+ setPath(() => [
+ { type: "M", x: 0, y: y1 },
+ { type: "L", x: x1, y: 0 },
+ { type: "L", x: x2, y: y2 },
+ { type: "V", y: h1 },
+ { type: "H", x: 0 },
+ { type: "Z" }])
+
+ }, [])
+
+ useEffect(() => {
+
+ console.log(getKoordnates())
+ setPositions(() => getKoordnates())
+ }, [path])
+
+ const getKoordnates = (): Position[] => {
+ let lastPoint = { x: 0, y: 0 };
+ let points: Position[] = [];
+ for (const p of path) {
+
+ let retValue: Position = { x: 0, y: 0, data: p }
+ switch (p.type) {
+ case "M":
+ lastPoint = { x: p.x, y: p.y };
+ points.push({ x: p.x, y: p.y, data: p });
+ break;
+ case "L":
+ lastPoint = { x: p.x, y: p.y };
+ points.push({ x: p.x, y: p.y, data: p });
+ break;
+ case "H":
+ retValue = { x: p.x, y: lastPoint.y, data: p }
+ lastPoint = { ...lastPoint, x: p.x };
+ points.push(retValue);
+ break;
+ case "V":
+ retValue = { x: lastPoint.x, y: p.y, data: p }
+ lastPoint = { ...lastPoint, y: p.y };
+ points.push(retValue);
+ break;
+ // case "Z":
+ // retValue = { x: -1, y: -1, data: p }
+ // lastPoint = { x: -1, y: -1 };
+ // points.push(retValue);
+ // break;
+
+ }
+ }
+
+ return points;
+
+
+
+ }
+
+ const drawPath = () => {
+
+ if (path.length == 0) return;
+
+ let pathValues = path.reduce((curent, cmdEle) => {
+ let value = ''
+ if (cmdEle.type === 'L') {
+ value = ` ${cmdEle.x} ${cmdEle.y}`
+ }
+ if (cmdEle.type === 'H') {
+ value = ` ${cmdEle.x}`
+ }
+ if (cmdEle.type === 'V') {
+ value = ` ${cmdEle.y}`
+ }
+ if (cmdEle.type === 'M') {
+ value = ` ${cmdEle.x} ${cmdEle.y}`
+ }
+
+ curent += ' ' + cmdEle.type + value;
+ return curent;
+ }, '')
+
+ return
+ //return {``}
+ }
+ if (path.length == 0) No Path
+ return (
+
+
+
+
+ {drawPath()}
+
+ {}
+
+
+
+
+
+ {false && positions.map((p, index) => )}
+
+
+ );
+}
+
+/*
+
+*/
\ No newline at end of file
diff --git a/src/components/visu/create/visuPoints.tsx b/src/components/visu/create/visuPoints.tsx
new file mode 100644
index 0000000..1fa1f08
--- /dev/null
+++ b/src/components/visu/create/visuPoints.tsx
@@ -0,0 +1,44 @@
+"use client"
+
+import { useEffect, useRef, useState } from "react";
+
+type Pos = { x: number; y: number };
+
+interface Props {
+ id:number
+ pos:Pos,
+ onSelect:(id:number)=>void
+}
+
+
+export default function VisuPoint({id,pos,onSelect }: Props) {
+
+ const [hasFocus,setHasFocus] = useState(false);
+
+ const handleMouseLeafe = ()=>{
+ console.log('Leaf')
+ setHasFocus(()=>false);
+ }
+
+ const handleMouseEnter = ()=>{
+ console.log('Enter')
+ setHasFocus(()=>true);
+ }
+
+ return (
+
+
+ onSelect(id)}
+ >
+
+
+
+ );
+}
+
+/*
+
+*/
\ No newline at end of file
diff --git a/src/components/visu/draw/svgShow.tsx b/src/components/visu/draw/svgShow.tsx
new file mode 100644
index 0000000..3bed010
--- /dev/null
+++ b/src/components/visu/draw/svgShow.tsx
@@ -0,0 +1,28 @@
+"use client"
+
+import { VisuElement } from "@/types/visu/visuElements.types";
+import { JSX, useState } from "react";
+
+interface Props {
+ svg: string;
+}
+
+//
+export function SvgShow({ svg }: Props) {
+
+
+
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/visu/svgElement copy.tsx b/src/components/visu/svgElement copy.tsx
new file mode 100644
index 0000000..20e2349
--- /dev/null
+++ b/src/components/visu/svgElement copy.tsx
@@ -0,0 +1,91 @@
+"use client"
+
+import { VisuElement } from "@/types/visu/visuElements.types";
+import { RefObject, useEffect, useRef } from "react";
+
+interface Props {
+ element: VisuElement,
+ selected: boolean,
+ onSelect: () => void,
+ //isPanningRef: RefObject
+ onDrag: (element: VisuElement, dx: number, dy: number) => void;
+ gridSize: number;
+}
+
+
+export function SvgVisuElement({ element, onSelect, onDrag, selected, gridSize }: Props) {
+ const draggingRef = useRef(false);
+ const isMouseDownRef = useRef(false);
+ const lastPos = useRef({ x: 0, y: 0 });
+
+
+ useEffect(() => {
+ const handleGlobalMouseUp = () => {
+ draggingRef.current = false;
+ isMouseDownRef.current = false;
+ };
+
+ window.addEventListener("mouseup", handleGlobalMouseUp);
+ return () => window.removeEventListener("mouseup", handleGlobalMouseUp);
+ }, []);
+
+ const handleMouseDown = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ draggingRef.current = false;
+ isMouseDownRef.current = true;
+ lastPos.current = { x: e.clientX, y: e.clientY };
+ };
+
+ const handleMouseMove = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ if (!isMouseDownRef.current) return; //
+ const dx = e.clientX - lastPos.current.x;
+ const dy = e.clientY - lastPos.current.y;
+
+ if (!draggingRef.current && (Math.abs(dx) > 2 || Math.abs(dy) > 2)) {
+ draggingRef.current = true;
+ }
+
+ if (draggingRef.current) {
+ onDrag(element, dx, dy);
+ lastPos.current = { x: e.clientX, y: e.clientY };
+ }
+ };
+
+ const handleMouseUp = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ //if (!draggingRef.current) {
+ // onSelect(element);
+ //}
+ draggingRef.current = false;
+ isMouseDownRef.current = false;
+ };
+
+ return (
+
+
+
+ {selected && }
+
+ {draggingRef.current ? DD : Normal}
+ {element.group?.nr ? {element.group.nr} : {element.id}}
+
+
+
+
+ );
+}
+
diff --git a/src/components/visu/svgElement.tsx b/src/components/visu/svgElement.tsx
new file mode 100644
index 0000000..36fe403
--- /dev/null
+++ b/src/components/visu/svgElement.tsx
@@ -0,0 +1,171 @@
+"use client"
+
+import { VisuElement } from "@/types/visu/visuElements.types";
+import { JSX, useState } from "react";
+
+interface Props {
+ element: VisuElement;
+ selected: boolean;
+ onElementMouseDown: (element: VisuElement, mouseEvent: React.MouseEvent) => void;
+}
+
+interface LineX {
+ pos: { x: number, y: number }[]
+}
+
+export function SvgVisuElement({ element, selected, onElementMouseDown }: Props) {
+
+ const [lines, setLinies] = useState({ pos: [{ x: 0, y: 0 }, { x: 100, y: 0 }, { x: 100, y: 50 }] })
+
+ const handleMouseDown = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ onElementMouseDown(element, e);
+ };
+
+
+
+ return (
+ handleMouseDown(e)}
+ style={{ cursor: selected ? "move" : "pointer" }}
+ >
+ {/* 🔥 HITBOX (oben!) */}
+
+
+ {/* SVG DARF KEINE EVENTS BLOCKEN */}
+
+
+ {element.lines &&
+
+ {element.lines.map((line, index) => {
+ const result: JSX.Element[] = [];
+ for (let i = 0; i < line.points.length - 1; i++) {
+ const p1 = line.points[i];
+ const p2 = line.points[i + 1];
+
+ result.push(
+
+ );
+ }
+
+ return {result};
+ })}
+
+
+
+ }
+
+
+
+ {/* Label */}
+ {element.group && !element.posAggElemente && (
+
+ {element.group.nr}
+
+ )}
+
+
+
+ {/* Auswahl */}
+ {selected && (
+
+ )}
+
+ );
+}
+
+/*
+ {element.group && element.posAggElemente && (
+
+
+
+ tt
+
+ )}
+
+ {element.posAggElemente && element.posAggElemente.posAnfahrwarnung &&
+
+ }
+
+ {element.posAggElemente && element.posAggElemente.posBetriebsart &&
+
+ }
+
+ {element.posAggElemente && element.posAggElemente.posMotor &&
+
+ }
+
+ {element.posAggElemente && element.posAggElemente.posFuIst &&
+
+ }
+
+
+*/
\ No newline at end of file
diff --git a/src/components/visu/utilities/snap.ts b/src/components/visu/utilities/snap.ts
new file mode 100644
index 0000000..b8e9dc1
--- /dev/null
+++ b/src/components/visu/utilities/snap.ts
@@ -0,0 +1,12 @@
+
+
+export const snapToGrid = (value: number, gridSize: number) => {
+ return Math.round(value / gridSize) * gridSize;
+};
+
+export const snapPointToGrid = (x: number, y: number, gridSize: number) => {
+ return {
+ x: snapToGrid(x, gridSize),
+ y: snapToGrid(y, gridSize),
+ };
+};
\ No newline at end of file
diff --git a/src/components/visu/visuAddGroups.tsx b/src/components/visu/visuAddGroups.tsx
new file mode 100644
index 0000000..7950956
--- /dev/null
+++ b/src/components/visu/visuAddGroups.tsx
@@ -0,0 +1,154 @@
+"use client"
+
+
+import { api_VisuElementCreate } from "@/actions/rk/api-visuElements";
+import GroupSelect from "@/components/rk/groups/groupSelect";
+
+import VisuElementList from "@/components/visu/visuElementList";
+import { Element, Group } from "@/types/rk";
+import { VisuElement, VisuElementCreate } from "@/types/visu/visuElements.types";
+import { useEffect, useState } from "react";
+import ElementSelect from "../rk/elements/elementSelect";
+
+interface Props {
+ groups: Group[],
+ elements: Element[]
+ visuElements: VisuElement[]
+}
+
+
+export default function VisuAddGroups({ groups, visuElements, elements }: Props) {
+
+ const [selectedGroup, setSelectedGroup] = useState()
+ const [selectedElement, setSelectedElement] = useState()
+ const [filteredGroups, setFilterdGroups] = useState([])
+ const [filteredElements, setFilterdElements] = useState([])
+ const [svgTypeName, setSvgTypeName] = useState()
+
+ useEffect(() => {
+ filterGroups();
+ filterElements()
+ }, [visuElements, groups,elements])
+
+ const handleSelectGroup = (group: Group) => {
+ setSelectedGroup(() => group);
+ }
+
+ const handleSelectElement = (element: Element) => {
+ setSelectedElement(() => element);
+ }
+
+ const filterGroups = () => {
+ const filterd = groups.filter((g) => {
+ const findGroupInElements = visuElements.find((ve) => ve.groupId == g.id);
+ if (!findGroupInElements) return g;
+ })
+
+ setFilterdGroups(() => filterd);
+ }
+
+ const filterElements = () => {
+ const filterd = elements.filter((el) => {
+ const findElementInVisuElements = visuElements.find((ve) => ve.elementId == el.id);
+ if (!findElementInVisuElements) return el;
+ })
+
+ setFilterdElements(() => filterd);
+ }
+
+ const onSaveGroup_old = () => {
+ if (!selectedGroup) return;
+ if (!svgTypeName) return;
+
+ let name = svgTypeName;
+ let variant = ""
+ if (svgTypeName === 'pumpensumpf') {
+ name = 'behaelter';
+ variant = 'pumpensumpf'
+ }
+
+ const data: VisuElementCreate = {
+ anlageId: selectedGroup.anlageId,
+ groupId: selectedGroup.id,
+ name: 'G_' + selectedGroup.nr + '_' + selectedGroup.name,
+ svgTypeName: name,
+ svgTypeVariant: variant,
+ posX: 100,
+ posY: 100
+
+ }
+ api_VisuElementCreate(data)
+ }
+
+ const onSave = () => {
+ if (!selectedElement) return;
+ if (!svgTypeName) return;
+
+ let name = svgTypeName;
+ let variant = ""
+ if (svgTypeName === 'pumpensumpf') {
+ name = 'behaelter';
+ variant = 'pumpensumpf'
+ }
+ const prefix = selectedElement.prefix ? "_"+ selectedElement.prefix : ""
+ const funcTxt = selectedElement.funcTxt ? "_"+ selectedElement.funcTxt : ""
+ const data: VisuElementCreate = {
+ anlageId: selectedElement.anlageId,
+ groupId: selectedElement.group.id,
+ elementId: selectedElement.id,
+ name: 'G_' + selectedElement.group.nr + prefix + '_' + selectedElement.group.name + funcTxt,
+ svgTypeName: name,
+ svgTypeVariant: variant,
+ posX: 100,
+ posY: 100
+
+ }
+ api_VisuElementCreate(data)
+ }
+
+ return (
+
+
+
+
+ {/* */}
+
+
+
+ {selectedElement ?
+
+
{selectedElement.group.nr}{selectedElement.prefix ? '_'+ selectedElement.prefix:"" }_{selectedElement.group.name}
+
+ - setSvgTypeName('band')}>Band
+ - setSvgTypeName('gurtfoerderer')}>Gurtfoerderer
+ - setSvgTypeName('sieb')}>Sieb
+ - setSvgTypeName('band')}>Band
+ - setSvgTypeName('setzmaschine')}>Setzmaschine
+ - setSvgTypeName('brecher')}>Brecher
+ - setSvgTypeName('behaelter')}>Behaelter
+ - setSvgTypeName('siebentwaesserung')}>Siebentwaesserung
+ - setSvgTypeName('weiche')}>Weiche
+ - setSvgTypeName('zyklon')}>Zyklon
+ - setSvgTypeName('turbowascher')}>Turbowascher
+ - setSvgTypeName('haspel')}>Haspel
+ - setSvgTypeName('silo')}>Silo
+ - setSvgTypeName('pumpensumpf')}>Pumpensumpf
+ - setSvgTypeName('pumpe')}>Pumpe
+ - setSvgTypeName('ventil')}>Ventil
+ - setSvgTypeName('wasserZufuhr')}>WasserZufuhr
+ - setSvgTypeName('motor')}>Motor
+
+
+ {svgTypeName ?
:
Bitt SVG Typ Auswählen
}
+
+ :
+
Select Group
+ }
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/visu/visuContent copy 2.tsxsave b/src/components/visu/visuContent copy 2.tsxsave
new file mode 100644
index 0000000..82bd1f7
--- /dev/null
+++ b/src/components/visu/visuContent copy 2.tsxsave
@@ -0,0 +1,273 @@
+"use client"
+
+import { useRef, useState, useEffect } from "react";
+import { SvgVisuElement } from "./svgElement";
+import { VisuElement } from "@/types/visu/visuElements.types";
+import { api_VisuElementUpdate } from "@/actions/rk/api-visuElements";
+import { useSvgDrag } from "@/hooks/useSvgDrag";
+
+interface Props {
+ elements: VisuElement[];
+}
+
+export default function VisuContent({ elements }: Props) {
+ const [viewBox, setViewBox] = useState({ x: 0, y: 0, w: 1000, h: 1000 });
+ const [selectedElement, setSelectedElement] = useState(null);
+ const [elementStates, setElementStates] = useState(elements);
+
+ const svgRef = useRef(null);
+ const gridSize = 20;
+
+ const frame = useRef(null);
+
+ const { startCanvasDrag, startElementDrag, dragState } = useSvgDrag({svgRef, gridSize, setElementStates, setViewBox,});
+
+
+
+ // 🔥 Screen → SVG
+ const screenToSvg = (clientX: number, clientY: number) => {
+ const svg = svgRef.current!;
+ const pt = svg.createSVGPoint();
+ pt.x = clientX;
+ pt.y = clientY;
+ const transformed = pt.matrixTransform(dragState.current.ctm!);
+ return { x: transformed.x, y: transformed.y };
+ };
+
+ // -----------------------
+ // 🖱 Canvas Pan
+ // -----------------------
+ const handleMouseDownCanvas = (e: React.MouseEvent) => {
+ const svg = svgRef.current!;
+ const ctm = svg.getScreenCTM()!.inverse();
+
+ const pt = svg.createSVGPoint();
+ pt.x = e.clientX;
+ pt.y = e.clientY;
+ const pos = pt.matrixTransform(ctm);
+
+ dragState.current = {
+ type: "canvas",
+ lastX: pos.x,
+ lastY: pos.y,
+ startX: 0,
+ startY: 0,
+ ctm,
+ };
+ };
+
+ // -----------------------
+ // 🖱 Element Drag Start
+ // -----------------------
+ const handleStartElementDrag = (element: VisuElement, e: React.MouseEvent) => {
+ e.stopPropagation();
+
+ const svg = svgRef.current!;
+ const ctm = svg.getScreenCTM()!.inverse();
+
+ const pt = svg.createSVGPoint();
+ pt.x = e.clientX;
+ pt.y = e.clientY;
+ const pos = pt.matrixTransform(ctm);
+
+ dragState.current = {
+ type: "element",
+ element,
+ lastX: pos.x,
+ lastY: pos.y,
+ startX: element.posX,
+ startY: element.posY,
+ ctm,
+ };
+ };
+
+ // -----------------------
+ // 🖱 GLOBAL MOVE + UP
+ // -----------------------
+ useEffect(() => {
+ const handleMouseMove = (e: MouseEvent) => {
+ if (frame.current) return;
+
+ frame.current = requestAnimationFrame(() => {
+ frame.current = null;
+
+ const state = dragState.current;
+ if (!state.type) return;
+
+ const pos = screenToSvg(e.clientX, e.clientY);
+ const dx = pos.x - state.lastX;
+ const dy = pos.y - state.lastY;
+
+ // 🔥 Element bewegen (stabil!)
+ if (state.type === "element" && state.element) {
+ state.startX += dx;
+ state.startY += dy;
+
+ const newX = Math.round(state.startX);
+ const newY = Math.round(state.startY);
+
+ setElementStates((prev) =>
+ prev.map((el) =>
+ el.id === state.element!.id
+ ? { ...el, posX: newX, posY: newY }
+ : el
+ )
+ );
+ }
+
+ // 🔥 Canvas bewegen
+ if (state.type === "canvas") {
+ setViewBox((prev) => ({
+ ...prev,
+ x: prev.x - dx,
+ y: prev.y - dy,
+ }));
+ }
+
+ state.lastX = pos.x;
+ state.lastY = pos.y;
+ });
+
+ };
+
+
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+
+
+
+ if (event.code === "Escape") {
+ setSelectedElement(null);
+ dragState.current.type = null;
+ //api_VisuElementUpdate(selectedElement.id, { posX: newPosX, posY: newPosY })
+ return;
+ }
+ const state = dragState.current;
+ if (state.type !== 'selected' || state.type == null) return;
+ let dx = 0;
+ let dy = 0;
+
+ if (!event.shiftKey && event.key === "ArrowRight") dx += 1;
+ if (event.shiftKey && event.key === "ArrowRight") dx += 10;
+ if (!event.shiftKey && event.key === "ArrowLeft") dx -= 1;
+ if (event.shiftKey && event.key === "ArrowLeft") dx -= 10;
+
+ if (!event.shiftKey && event.key === "ArrowDown") dy += 1;
+ if (event.shiftKey && event.key === "ArrowDown") dy += 10;
+ if (!event.shiftKey && event.key === "ArrowUp") dy -= 1;
+ if (event.shiftKey && event.key === "ArrowUp") dy -= 10;
+ if (dx == 0 && dy == 0 || !state.element) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+ if (state) {
+ setElementStates((prev) =>
+ prev.map((el) => {
+ if (el.id !== state.element!.id) return el; // 2
+
+ let newX = el.posX + dx;
+ let newY = el.posY + dy;
+
+ dragState.current.lastX = newX;
+ dragState.current.lastY = newY;
+ let element = { ...el, posX: newX, posY: newY };
+ console.log("se")
+ setSelectedElement(() => element)
+ return element;
+ })
+ );
+ }
+ }
+
+ const handleMouseUp = () => {
+ const state = dragState.current;
+
+ if (state.type === "element" && state.element) {
+ setElementStates((prev) =>
+ prev.map((el) => {
+ if (el.id !== state.element!.id) return el;
+
+ return {
+ ...el,
+ posX: Math.round(el.posX / gridSize) * gridSize,
+ posY: Math.round(el.posY / gridSize) * gridSize,
+ };
+ })
+ );
+
+ dragState.current.type = "selected";
+ } else {
+ dragState.current.type = null;
+ }
+ };
+
+
+ window.addEventListener("keydown", handleKeyDown);
+ window.addEventListener("mousemove", handleMouseMove);
+ window.addEventListener("mouseup", handleMouseUp);
+
+ return () => {
+ window.removeEventListener("mousemove", handleMouseMove);
+ window.removeEventListener("mouseup", handleMouseUp);
+ };
+ }, []);
+
+ // -----------------------
+ // 🎯 Grid
+ // -----------------------
+ const renderGrid = () => {
+ const lines = [];
+
+ for (let x = 0; x < 2560; x += gridSize) {
+ lines.push(
+
+ );
+ }
+
+ for (let y = 0; y < 1440; y += gridSize) {
+ lines.push(
+
+ );
+ }
+
+ return {lines};
+ };
+
+ // -----------------------
+ // 🎨 Render
+ // -----------------------
+ return (
+
+
{JSON.stringify(selectedElement)}
+
{JSON.stringify(dragState.current)}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/visu/visuContent copy.tsxsave b/src/components/visu/visuContent copy.tsxsave
new file mode 100644
index 0000000..64ab3cb
--- /dev/null
+++ b/src/components/visu/visuContent copy.tsxsave
@@ -0,0 +1,239 @@
+
+"use client"
+
+import { SvgVisuElement } from "./svgElement";
+import { useRef, useState } from "react";
+import { api_VisuElementUpdate } from "@/actions/rk/api-visuElements";
+import { VisuElement } from "@/types/visu/visuElements.types";
+
+interface Props {
+ elements: VisuElement[]
+}
+/**
+ *
+🔲 Element Drag (nicht nur Canvas)
+📦 Resize Handles
+🧲 Snap to Grid
+🔍 Zoom Limits + Centering
+*/
+export default function VisuContent({ elements }: Props) {
+ const [selectedElement, setSelectedElement] = useState();
+ const [elementStates, setElementStates] = useState(elements);
+ const [viewBox, setViewBox] = useState({ x: 0, y: 0, w: 1000, h: 1000 });
+
+ const isPanningRef = useRef(false);
+ const isMouseDown = useRef(false);
+ const lastPos = useRef({ x: 0, y: 0 });
+ const gridSize = 20;
+
+
+ const clamp = (val: number, min: number, max: number) => Math.max(min, Math.min(max, val));
+
+
+
+ const dragThreshold = 5;
+
+ const handleMouseDown = (e: React.MouseEvent) => {
+ //if (!isSpacePressed) return; // 🔥 KEY FIX
+
+ lastPos.current = { x: e.clientX, y: e.clientY };
+ isPanningRef.current = false;
+ isMouseDown.current = true;
+
+
+ };
+
+ const handleMouseUp = () => {
+ isPanningRef.current = false;
+ isMouseDown.current = false;
+
+ };
+
+ const handleMouseMove = (e: React.MouseEvent) => {
+ if (!isMouseDown.current) return;
+
+ const dx = e.clientX - lastPos.current.x;
+ const dy = e.clientY - lastPos.current.y;
+ console.log(isPanningRef.current);
+ if (!isPanningRef.current && (Math.abs(dx) > 2 || Math.abs(dy) > 2)) {
+ isPanningRef.current = true;
+ }
+ if (isPanningRef.current) {
+ const rect = e.currentTarget.getBoundingClientRect();
+ const scaleX = viewBox.w / rect.width;
+ const scaleY = viewBox.h / rect.height;
+ setViewBox((prev) => ({
+ ...prev,
+ x: prev.x - dx * scaleX,
+ y: prev.y - dy * scaleY,
+ }));
+ lastPos.current = { x: e.clientX, y: e.clientY };
+ }
+ };
+
+
+
+ const handleWheel = (e: React.WheelEvent) => {
+ e.preventDefault();
+ const zoomFactor = e.deltaY > 0 ? 1.1 : 1 / 1.1;
+ const rect = e.currentTarget.getBoundingClientRect();
+ const mouseX = e.clientX - rect.left;
+ const mouseY = e.clientY - rect.top;
+ const scaleX = viewBox.w / rect.width;
+ const scaleY = viewBox.h / rect.height;
+ const svgX = viewBox.x + mouseX * scaleX;
+ const svgY = viewBox.y + mouseY * scaleY;
+ const newW = clamp(viewBox.w * zoomFactor, 200, 2560);
+ const newH = clamp(viewBox.h * zoomFactor, 200, 1440);
+ setViewBox({
+ x: svgX - (mouseX / rect.width) * newW,
+ y: svgY - (mouseY / rect.height) * newH,
+ w: newW,
+ h: newH,
+ });
+ };
+
+
+
+ const handleSelectElement = (element: VisuElement) => {
+ setSelectedElement(element);
+ }
+
+ const handleChangeParameter = (changeData: { posX?: number, posY?: number }) => {
+ console.log(changeData)
+ if (!selectedElement) return;
+ setSelectedElement((c) => c ? { ...c, ...changeData } : undefined)
+ api_VisuElementUpdate(selectedElement.id, { ...changeData })
+ }
+
+ const handleKeyUp = (event: React.KeyboardEvent) => {
+ //if (event.code === "Space") setIsSpacePressed(false);
+ };
+
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ //if (event.code === "Space") setIsSpacePressed(true);
+
+ event.preventDefault();
+
+ if (event.code === "Escape") setSelectedElement(undefined);
+ if (!selectedElement) return;
+
+ let oldPosX = selectedElement.posX;
+ let newPosX = oldPosX;
+
+ let oldPosY = selectedElement.posY;
+ let newPosY = oldPosY;
+
+ if (!event.shiftKey && event.key === "ArrowRight") newPosX += 1;
+ if (event.shiftKey && event.key === "ArrowRight") newPosX += 10;
+ if (!event.shiftKey && event.key === "ArrowLeft") newPosX -= 1;
+ if (event.shiftKey && event.key === "ArrowLeft") newPosX -= 10;
+
+ if (!event.shiftKey && event.key === "ArrowDown") newPosY += 1;
+ if (event.shiftKey && event.key === "ArrowDown") newPosY += 10;
+ if (!event.shiftKey && event.key === "ArrowUp") newPosY -= 1;
+ if (event.shiftKey && event.key === "ArrowUp") newPosY -= 10;
+
+ if (oldPosX != newPosX || oldPosY != newPosY) {
+ if (selectedElement) setSelectedElement((c) => c ? { ...c, posX: newPosX, posY: newPosY } : undefined)
+ api_VisuElementUpdate(selectedElement.id, { posX: newPosX, posY: newPosY })
+ }
+ }
+
+ // Element Drag mit Snap
+ const handleDragElement = (element: VisuElement, dx: number, dy: number) => {
+ setElementStates((prev) =>
+ prev.map((el) => {
+ if (el.id === element.id) {
+ const scaleX = viewBox.w / 1000;
+ const scaleY = viewBox.h / 1000;
+ let newX = el.posX + dx * scaleX;
+ let newY = el.posY + dy * scaleY;
+
+ // Snap-to-Grid
+ newX = Math.round(newX / gridSize) * gridSize;
+ newY = Math.round(newY / gridSize) * gridSize;
+
+ return { ...el, posX: newX, posY: newY };
+ }
+ return el;
+ })
+ );
+ };
+ // Grid Linien rendern
+ const renderGrid = () => {
+ const lines = [];
+ for (let x = 0; x < 2560; x += gridSize) {
+ lines.push();
+ }
+ for (let y = 0; y < 1440; y += gridSize) {
+ lines.push();
+ }
+ return {lines};
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {selectedElement &&
+
handleChangeParameter({ posX: Number.parseFloat(v.target.value) })} />
+
handleChangeParameter({ posY: Number.parseFloat(v.target.value) })} />
+
+
Höhe
+
{selectedElement.height}
+
+
+
+ }
+
+
+ );
+}
+
+/*
+
+ style={{ transform: `scale(${1.0})`, transformOrigin: "top left" }}
+*/
\ No newline at end of file
diff --git a/src/components/visu/visuContent.tsx b/src/components/visu/visuContent.tsx
new file mode 100644
index 0000000..3b6b3c5
--- /dev/null
+++ b/src/components/visu/visuContent.tsx
@@ -0,0 +1,72 @@
+"use client"
+
+import { useRef, useState } from "react";
+import { SvgVisuElement } from "./svgElement";
+import { useSvgDrag } from "@/hooks/useSvgDrag";
+import { useSvgKey } from "@/hooks/useSvgKey";
+import { useVisuElementContext } from "@/_context/visu/useVisuElementsContext";
+import VisuElementProps from "./visuElementProps";
+import VisuContentGrid from "./visuContentGrid";
+
+interface Props {
+ //elements: VisuElement[];
+}
+
+export default function VisuContent({ }: Props) {
+ const height = 1120;
+ const width = 1810;
+ const raster = 20;
+
+ const { state, dispatch } = useVisuElementContext();
+ const [viewBox, setViewBox] = useState({ x: 0, y: 0, w: 1820, h: 1120 });
+ const svgRef = useRef(null);
+
+
+
+ const { startCanvasDrag, startElementDrag, handleWheel } = useSvgDrag({ svgRef, gridSize: raster, setViewBox, });
+ const {onKeyDown } = useSvgKey({enabled:true} )
+
+
+ return (
+
+
+
+
+ {state.selectedElement &&
console.log('on change n.v.') }/>}
+
+ {true &&
+
+
{JSON.stringify(state.selectedElement)}
+
{JSON.stringify(state.drag)}
+
+ }
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/visu/visuContentGrid.tsx b/src/components/visu/visuContentGrid.tsx
new file mode 100644
index 0000000..2a662b2
--- /dev/null
+++ b/src/components/visu/visuContentGrid.tsx
@@ -0,0 +1,32 @@
+
+interface Props {
+ height:number;
+ width:number;
+ raster:number;
+}
+
+export default function VisuContentGrid({ height,width,raster }: Props) {
+ const renderGrid = () => {
+ const Lines = [];
+
+ for (let x = 0; x < width; x += raster) {
+ Lines.push();
+ }
+
+ for (let y = 0; y < height; y += raster) {
+ Lines.push();
+ }
+ Lines.push()
+
+ return Lines;
+ };
+
+
+
+return
+
+ {renderGrid()}
+
+
+
+}
\ No newline at end of file
diff --git a/src/components/visu/visuElementList.tsx b/src/components/visu/visuElementList.tsx
new file mode 100644
index 0000000..49fe1bb
--- /dev/null
+++ b/src/components/visu/visuElementList.tsx
@@ -0,0 +1,39 @@
+import { VisuElement } from "@/types/visu/visuElements.types";
+
+
+
+interface Props {
+ elements: VisuElement[],
+ onSelect?: (item: VisuElement) => void
+}
+
+export default function VisuElementListOld({ elements, onSelect }: Props) {
+
+ const handleSelect = (item: VisuElement) => {
+ if (onSelect) onSelect(item);
+ }
+
+ return (
+
+
+ {elements.map((item) => (
+
handleSelect(item)} key={item.id}>
+ {item.group ?
+
{`${item.group.nr} ${item.group.name}`}
+ :
+
{`${item.id} no Group`}
+ }
+ {item.elementId ?
+
{item.elementId}
+ :
+
{`${item.id} no Element`}
+ }
+
+ )
+ )}
+
+
+ )
+}
+
+
diff --git a/src/components/visu/visuElementProps.tsx b/src/components/visu/visuElementProps.tsx
new file mode 100644
index 0000000..dbbbc0c
--- /dev/null
+++ b/src/components/visu/visuElementProps.tsx
@@ -0,0 +1,164 @@
+"use client"
+
+import { useState, useEffect } from "react";
+import { VisuElement } from "@/types/visu/visuElements.types";
+import { api_VisuElementUpdate } from "@/actions/rk/api-visuElements";
+import VisuLineProps from "./visuLineProps";
+import { VisuLine, VisuLinePoint } from "@/types/visu/visuLine.types";
+import { useVisuElementContext } from "@/_context/visu/useVisuElementsContext";
+import { useNumberInput } from "@/hooks/useNumberInput";
+
+interface Props {
+
+ onChange: (updatedElement: VisuElement) => void;
+}
+
+export default function VisuElementProps({ onChange }: Props) {
+ const { state, dispatch } = useVisuElementContext();
+
+
+
+
+
+ // -----------------------
+ // 🔄 UPDATE STATE ONLY
+ // -----------------------
+ const updateField = (field: K) => {
+ let element = state.selectedElement;
+
+ return (value: VisuElement[K]) => {
+ if(!element) return;
+ dispatch({
+ type: "UPDATE_ELEMENT",
+ payload: {
+ ...element,
+ [field]: value,
+ },
+ });
+
+ api_VisuElementUpdate(element.id, {
+ [field]: value,
+ });
+ };
+ };
+ if(!state.selectedElement) return No Selection
+ const scaleInput = useNumberInput({ value: state.selectedElement.scale, fallback: 1, min: 0.1, onCommit: updateField("scale"), });
+ const widthInput = useNumberInput({ value: state.selectedElement.width, fallback: 5, min: 1, onCommit: updateField("width"), });
+ const heightInput = useNumberInput({ value: state.selectedElement.height, fallback: 5, min: 1, onCommit: updateField("height"), });
+ const rasterInput = useNumberInput({ value: state.selectedElement.raster, fallback: 5, min: 1, onCommit: updateField("raster"), });
+ const posXInput = useNumberInput({ value: state.selectedElement.posX, fallback: 5, min: 1, onCommit: updateField("posX"), });
+ const posYInput = useNumberInput({ value: state.selectedElement.posY, fallback: 5, min: 1, onCommit: updateField("posY"), });
+ const angleInput = useNumberInput({ value: state.selectedElement.angle, fallback: 0, onCommit: updateField("angle"), });
+
+
+
+ const handleChangeLine = (line: VisuLine, index: number) => {
+ if (!state.selectedElement?.lines) return;
+
+ const lines = state.selectedElement.lines;
+ if (lines && index < lines.length) {
+ lines[index] = line;
+ api_VisuElementUpdate(state.selectedElement.id, { lines: lines })
+ }
+ }
+
+
+ const addLine = ()=>{
+ let element = state.selectedElement;
+ if(!element) return;
+ if(!element.lines) element['lines'] = [];
+
+ element?.lines?.push({bezeichnung:"ausgang",color:"#202020",offsetX:0,offsetY:0,points:[{x:0,y:0},{x:0,y:50},{x:50,y:50}]});
+ console.log(element)
+ dispatch({type:"UPDATE_ELEMENT",payload:element})
+
+ }
+
+
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{state.selectedElement.px_width}
+
+
+
+
+
+
{state.selectedElement.px_height}
+
+
+ {
}
+ {state.selectedElement.lines?.map((line, index) =>
handleChangeLine(line, index)}>)}
+
+
+
+ )
+
+}
\ No newline at end of file
diff --git a/src/components/visu/visuElements/visuElementItem.tsx b/src/components/visu/visuElements/visuElementItem.tsx
new file mode 100644
index 0000000..89a3c73
--- /dev/null
+++ b/src/components/visu/visuElements/visuElementItem.tsx
@@ -0,0 +1,56 @@
+"use client"
+
+
+import { useEffect, useState } from "react";
+
+import { emptyVisuElement, VisuElement, VisuElementCreate, VisuElementUpdate } from "@/types/visu/visuElements.types";
+import { useForm } from "react-hook-form";
+import { InputTextR } from "@/components/ui/inputTextR";
+import { api_VisuElementUpdate } from "@/actions/rk/api-visuElements";
+import { Element } from "@/types/rk";
+import { SelectR } from "@/components/ui/selectR";
+
+
+interface Props {
+ visuElement: VisuElement,
+ elements: Element[]
+}
+
+export default function VisuElementItem({ visuElement, elements }: Props) {
+
+ const { control, register, handleSubmit, formState: { errors }, watch, getFieldState, reset } = useForm({ defaultValues: emptyVisuElement, })
+ const [id, setId] = useState("");
+
+ const cols = [300, 200, 300];
+
+ useEffect(() => {
+ if (visuElement) {
+ const { id,elementId, ...rest } = visuElement;
+ setId(id);
+ reset({ ...rest,elementId })
+ }
+ }, [visuElement]);
+
+ const onSubmit = async (data: VisuElementUpdate) => {
+ console.log('submitt')
+ await api_VisuElementUpdate(id,data);
+
+ }
+
+
+ return (
+
+ )
+}
+
+
diff --git a/src/components/visu/visuElements/visuElementsList.tsx b/src/components/visu/visuElements/visuElementsList.tsx
new file mode 100644
index 0000000..32a255e
--- /dev/null
+++ b/src/components/visu/visuElements/visuElementsList.tsx
@@ -0,0 +1,60 @@
+"use client"
+
+
+import { useEffect, useState } from "react";
+
+import { VisuElement } from "@/types/visu/visuElements.types";
+import VisuElementItem from "./visuElementItem";
+import { Element } from "@/types/rk";
+
+
+interface Props {
+ visuElements: VisuElement[],
+ elements:Element[]
+
+}
+
+export default function VisuElementsList({ visuElements,elements }: Props) {
+
+ const cols = [300, 200, 300];
+
+ const head = () => {
+ return `${c}px`).join(" ")
+ }}>
+
Group
+
Name
+
SVG Name
+
+
+ }
+
+ const show = () => {
+ return visuElements.map((item) =>
+ `${c}px`).join(" ")
+ }}
+ >
+
{item.group.nr}_{item.group.name}
+
+
+
+
)
+ }
+
+
+
+
+ return (
+
+ {head()}
+ {show()}
+
+
+ )
+}
+
+
diff --git a/src/components/visu/visuLinePointInput.tsx b/src/components/visu/visuLinePointInput.tsx
new file mode 100644
index 0000000..ba6a053
--- /dev/null
+++ b/src/components/visu/visuLinePointInput.tsx
@@ -0,0 +1,60 @@
+"use client"
+
+import { VisuLinePoint } from "@/types/visu/visuLine.types";
+import { useNumberInput } from "@/hooks/useNumberInput";
+
+interface Props {
+ index: number;
+ point: VisuLinePoint;
+
+ onChangePoints: (index: number, point: Partial) => void;
+}
+
+
+
+export default function VisuLinePointInput({ index, point, onChangePoints }: Props) {
+
+
+ const update = (field: 'x' | 'y', value: number) => {
+
+ let point: Partial = {}
+ if (field === 'x') point['x'] = value;
+ if (field === 'y') point['y'] = value;
+
+
+ onChangePoints(index, point);
+ }
+
+ const posX = useNumberInput({ value: point.x, onCommit: (value) => update('x', value), });
+ const posY = useNumberInput({ value: point.y, onCommit: (value) => update('y', value), });
+
+
+
+
+
+ return (
+
+
+
+
{index}
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+
+}
\ No newline at end of file
diff --git a/src/components/visu/visuLineProps.tsx b/src/components/visu/visuLineProps.tsx
new file mode 100644
index 0000000..093f7f0
--- /dev/null
+++ b/src/components/visu/visuLineProps.tsx
@@ -0,0 +1,106 @@
+"use client"
+
+import { VisuLine, VisuLinePoint } from "@/types/visu/visuLine.types";
+import VisuLinePointInput from "./visuLinePointInput";
+import { useNumberInput } from "@/hooks/useNumberInput";
+
+
+interface Props {
+ line: VisuLine;
+ onChangePoints: (points: VisuLine) => void;
+}
+
+
+
+export default function VisuLineProps({ line, onChangePoints }: Props) {
+
+ const offsetX = useNumberInput({ value: line.offsetX, onCommit: (value) => update('offsetX', value), });
+ const offsetY = useNumberInput({ value: line.offsetY, onCommit: (value) => update('offsetY', value), });
+
+
+ const changeLinePoint = (index: number, point: Partial) => {
+ let newLine = { ...line }
+ let points = [...newLine.points];
+
+ if (point.x != undefined) points[index].x = point.x;
+ if (point.y != undefined) points[index].y = point.y;
+ //if(!newLine.points) newLine['points'] = []
+ newLine['points'] = points;
+ onChangePoints(newLine);
+
+ }
+
+ const update = (field: 'offsetX' | 'offsetY', value: number) => {
+ let newLine = { ...line }
+
+ let point: Partial = {}
+ if (field === 'offsetX') newLine['offsetX'] = value;
+ if (field === 'offsetY') newLine['offsetY'] = value;
+
+
+
+ onChangePoints(newLine);
+ }
+
+ const addPoint = (index: number) => {
+ let newLine = { ...line }
+ let points = newLine.points;
+ newLine.points.splice(index + 1, 0, { x: 0, y: 0 });
+ let point: Partial = {}
+ onChangePoints(newLine);
+
+ }
+
+ const removePoint = (index: number) => {
+ let newLine = { ...line }
+
+ newLine.points.splice(index, 1);
+
+ onChangePoints(newLine);
+
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {line.points.map((point, index) =>
+
+
+
+
+
+ )}
+
+
+
+ )
+
+}
\ No newline at end of file
diff --git a/src/components/visu/visuMousePostions.tsx b/src/components/visu/visuMousePostions.tsx
new file mode 100644
index 0000000..876e03c
--- /dev/null
+++ b/src/components/visu/visuMousePostions.tsx
@@ -0,0 +1,47 @@
+"use client"
+
+import { useEffect, useRef, useState } from "react";
+
+
+
+interface Props {
+}
+
+type Pos = { x: number; y: number };
+type Point = { id: number, x: number; y: number };
+
+export default function VisuCreatePlatform({ }: Props) {
+ const x = 0;
+ const y = 0;
+
+ const [pos, setPos] = useState({ x: 0, y: 0 });
+ const frame = useRef(null);
+ const latest = useRef({ x: 0, y: 0 });
+
+ const handleMouseMove = (e: React.MouseEvent) => {
+ const rect = e.currentTarget.getBoundingClientRect();
+
+ latest.current = {
+ x: e.clientX - rect.left,
+ y: e.clientY - rect.top,
+ };
+
+ if (frame.current === null) {
+ frame.current = requestAnimationFrame(() => {
+ setPos(latest.current);
+ frame.current = null;
+ });
+ }
+ };
+
+
+ return (
+
+
Mausposition
+
X: {pos.x}
+
Y: {pos.y}
+
+
+ );
+}
+
diff --git a/src/gloabls.d.ts b/src/gloabls.d.ts
new file mode 100644
index 0000000..7f70183
--- /dev/null
+++ b/src/gloabls.d.ts
@@ -0,0 +1 @@
+declare module "*.css";
\ No newline at end of file
diff --git a/src/hooks/procon/useVisuAgg.ts b/src/hooks/procon/useVisuAgg.ts
new file mode 100644
index 0000000..44b35a6
--- /dev/null
+++ b/src/hooks/procon/useVisuAgg.ts
@@ -0,0 +1,83 @@
+
+
+import { xamlToSvg } from "@/components/procon/svg/xaml_to_svg";
+import { ProConView } from "@/types/procon";
+import { useEffect, useState } from "react";
+
+interface Props {
+ data: ProConView[];
+}
+
+interface Graphics {
+ viewType:string,
+ viewIndex:number,
+ state:string,
+ xamlId:string,
+ xaml:string,
+ svg:string
+}
+
+export function useVisuAgg({ data }: Props) {
+ const [selectedViewElement, setSelectedViewElement] = useState();
+
+ const [graphics, setGraphics] = useState([]);
+ const [elements, setElements] = useState([])
+ const [ele, setEle] = useState<{ name: string, index: number }[]>([])
+
+ useEffect(() => {
+ let elmentsOrdered = orderElements(data);
+ setElements(() => elmentsOrdered);
+ setGraphics(()=>[]);
+ let eleNames: { name: string, index: number }[] = [];
+ elmentsOrdered.map((view, index) => {
+ if (view.viewType === "agg") eleNames.push({ name: "Aggregat", index })
+ if (view.viewType === "motor") eleNames.push({ name: "Motor", index })
+ if (view.viewType === "motor2") eleNames.push({ name: "Motor 2", index })
+ if (view.viewType === "betriebsart") eleNames.push({ name: "Betriebsart", index })
+ if (view.viewType === "warnung") eleNames.push({ name: "Warnung", index })
+ if (view.viewType === "anfahrzeit") eleNames.push({ name: "Anfahrzeit", index })
+ if (view.viewType === "text") eleNames.push({ name: "BMK", index })
+ if (view.viewType === "pfeil") eleNames.push({ name: "Pfeil " + view.viewIndex, index })
+ let g:Graphics[] = view.views.map((xamlData)=>{
+ return {
+ viewType:view.viewType,
+ viewIndex:view.viewIndex,
+ state:xamlData.state,
+ xamlId:xamlData.id,
+ xaml:atob(xamlData.xaml),
+ svg:convertXamlToSvg(atob(xamlData.xaml))
+ }
+ })
+ setGraphics((cur)=>[...cur,...g]);
+ })
+ setEle(() => eleNames);
+
+ }, [elements])
+
+ const convertXamlToSvg = (xaml:string)=>{
+ let svg = xamlToSvg(xaml);
+ return svg;
+ }
+
+ const orderElements = (elements: ProConView[]) => {
+ const order = ['agg', 'motor','motor2', 'betriebsart', 'warnung', 'anfahrzeit', 'text', 'pfeil']
+
+ // Erste Sotierung
+ let x1 = elements.sort((a, b) => (a.viewIndex - b.viewIndex))
+
+ // 2. sotierung
+ return x1.sort((a, b) => order.indexOf(a.viewType) - order.indexOf(b.viewType))
+ }
+
+ const onSelectViewElement = (index:number)=>{
+ setSelectedViewElement(()=>elements[index])
+ }
+
+ return {
+ elements: elements,
+ viewNames: ele,
+ selectedViewElement,
+ onSelectViewElement,
+ graphics
+ };
+}
\ No newline at end of file
diff --git a/src/hooks/rk2/useComponent.ts b/src/hooks/rk2/useComponent.ts
new file mode 100644
index 0000000..c0a10d4
--- /dev/null
+++ b/src/hooks/rk2/useComponent.ts
@@ -0,0 +1,195 @@
+import { api_component_create, api_component_remove, api_component_update } from "@/actions/rk2/api-component";
+import { api_signals_create } from "@/actions/rk2/api-signals";
+import { ComponentCreate, ComponentDefaults, ComponentResponse, ComponentType, ComponentTypeDefinition, ComponentUpdate, PropertyDefinition } from "@/types/anlagen/components";
+import { useCallback, useEffect, useState } from "react";
+
+interface Props {
+ component?: ComponentResponse,
+ componentTypes: ComponentTypeDefinition[]
+ componentDefaults: ComponentDefaults[]
+}
+interface ComponentNames {
+ id: string,
+ name: string
+}
+
+const emptyComponent: ComponentResponse = {
+ id: "",
+ name: "",
+ type: "",
+ index: 0,
+ postfix: "",
+ prefix: "",
+ properties: {}
+
+}
+
+export function useComponent({ component, componentTypes, componentDefaults }: Props) {
+
+ const [data, setData] = useState(emptyComponent);
+ const [componentNames, setComponentNames] = useState([])
+ const [loading, setLoading] = useState(false);
+
+
+ //-------------------------------------------------------------------------
+ //--- init component Data
+ //-------------------------------------------------------------------------
+ useEffect(() => {
+ console.log('Render',component?.properties)
+ if (!component)
+ setData(() => ({ ...emptyComponent,properties:{} }))
+ else
+ setData(() => ({ ...emptyComponent,properties:{}, ...component }))
+
+ }, [component])
+
+
+ //-------------------------------------------------------------------------
+ //--- init Components Names
+ //-------------------------------------------------------------------------
+ useEffect(() => {
+ let compNames: ComponentNames[] = [];
+
+ componentTypes.forEach((c) => compNames.push({ id: c.type, name: c.label }))
+ setComponentNames(() => compNames);
+ }, [componentTypes])
+
+
+ //-------------------------------------------------------------------------
+ //--- set Values of Compnent
+ //-------------------------------------------------------------------------
+ const setValue = (fieldName: K, value: ComponentResponse[K]): void => {
+ setData((cur) => ({ ...cur, [fieldName]: value }))
+ }
+
+ //-------------------------------------------------------------------------
+ //--- Suche Type von Componente
+ //-------------------------------------------------------------------------
+ const findCompType = (type: string): ComponentTypeDefinition | undefined => {
+ return componentTypes.find((item) => item.type === type)
+ }
+
+ //-------------------------------------------------------------------------
+ //--- Propertie Typ Suchen
+ //-------------------------------------------------------------------------
+ const findCompPropertyType = (propName: string): PropertyDefinition | undefined => {
+ let comType = findCompType(data.type);
+ let propType = comType?.properties.find((p) => p.key === propName);
+
+ return propType
+ }
+
+
+ //-------------------------------------------------------------------------
+ //--- Propertie Änderung
+ //-------------------------------------------------------------------------
+ const changePropertie = (name: string, value: unknown) => {
+
+ let newData = { ...data };
+ if (!newData.properties) return;
+ newData.properties[name] = value;
+
+ setData((cur) => ({ ...cur, properties: newData.properties }));
+ }
+
+ //-------------------------------------------------------------------------
+ //--- Default Properties hinzufügen
+ //-------------------------------------------------------------------------
+ const setDefaultPropertie = () => {
+
+ let newData = { ...data }; // Compenenten daten kopieren
+ let comType = findCompType(data.type);
+ if(!comType) return;
+ let currentProperties = newData.properties;
+ if(!currentProperties) currentProperties = {};
+
+ comType.properties.forEach((dp)=>{
+ if(!currentProperties[dp.key]) currentProperties[dp.key] = dp.defaultValue;
+ })
+
+
+
+ setData((cur) => ({ ...cur, properties: currentProperties }));
+ }
+
+ //-------------------------------------------------------------------------
+ //--- Defaults
+ //-------------------------------------------------------------------------
+ const getCompDefaults = (compType: ComponentType) => {
+ let defaults = componentDefaults.find((c) => c.type === compType);
+ if (!defaults) defaults = { name: "unbekannt", signals: [], type: "unbekannt" }
+
+ return defaults;
+ }
+
+
+
+
+
+ //-------------------------------------------------------------------------
+ //--- Api Create
+ //-------------------------------------------------------------------------
+ const createComponent = async (unitId: string, data: ComponentCreate) => {
+ let defaults = getCompDefaults(data.type);
+ let comTyp = findCompType(data.type);
+ if (!data.name || data.name == "") data.name = defaults.name;
+
+ if (!comTyp) return;
+
+ const record = Object.fromEntries(
+ comTyp.properties.map(({ key, type, defaultValue }) => [key, defaultValue])
+ );
+
+ if (comTyp && comTyp.properties) data.properties = record;
+ console.log(data)
+ setLoading(true);
+ let resultCreatComp = await api_component_create(unitId, data);
+ if (resultCreatComp.success) {
+ for (let defSig of defaults.signals) {
+ await api_signals_create({ name: defSig.name, type: defSig.type, componentId: resultCreatComp.data.id })
+
+ }
+
+ }
+ setLoading(false);
+
+ }
+
+
+ //-------------------------------------------------------------------------
+ //--- Api Update
+ //-------------------------------------------------------------------------
+ const updateComponent = async (compId: string, data: ComponentResponse) => {
+
+ let { id, ...update } = data;
+ setLoading(true);
+ await api_component_update(compId, update);
+ setLoading(false);
+ }
+
+ //-------------------------------------------------------------------------
+ //--- Api Remove
+ //-------------------------------------------------------------------------
+ const removeComponent = async (compId: string) => {
+ setLoading(true);
+ await api_component_remove(compId);
+ setLoading(false);
+ }
+
+
+
+
+ return {
+ loading,
+ compnentData: data,
+ setValue,
+ componentNames,
+ findCompType,
+ findCompPropertyType,
+ createComponent,
+ updateComponent,
+ removeComponent,
+ changePropertie,
+ setDefaultPropertie
+ };
+}
\ No newline at end of file
diff --git a/src/hooks/rk2/useGroup.ts b/src/hooks/rk2/useGroup.ts
new file mode 100644
index 0000000..8edcc47
--- /dev/null
+++ b/src/hooks/rk2/useGroup.ts
@@ -0,0 +1,27 @@
+
+import { GroupCreate, GroupResponse } from "@/types/anlagen/groups";
+
+
+import { useCallback } from "react";
+
+interface Props {
+ group: GroupResponse | undefined;
+
+}
+
+export function useGroup({ group }: Props) {
+
+ const emptyGroup = {
+
+ name: "",
+ container: true,
+ nr: "",
+ parentId: ""
+ }
+
+
+ return {
+ emptyGroup
+
+ };
+}
\ No newline at end of file
diff --git a/src/hooks/rk2/useGroupTree.ts b/src/hooks/rk2/useGroupTree.ts
new file mode 100644
index 0000000..9ed12f3
--- /dev/null
+++ b/src/hooks/rk2/useGroupTree.ts
@@ -0,0 +1,183 @@
+import { GroupResponse } from "@/types/anlagen/groups"
+import { useCallback, useMemo, useState } from "react"
+
+
+
+export interface GroupTree {
+ group: GroupResponse
+ childs: GroupTree[]
+}
+
+export function useGroupTree(groups: GroupResponse[]) {
+
+ const [openGroups, setOpenGroups] = useState>(new Set())
+ const [selectedGroupId, setSelectedGroupId] = useState(null)
+
+ /**
+ * Erstellt aus dem flachen Array den Tree.
+ */
+ const tree = useMemo(() => {
+ const tree: GroupTree[] = []
+
+ const map = new Map()
+
+ // Alle Nodes erstellen
+ for (const group of groups) {
+ map.set(group.id, {
+ group,
+ childs: [],
+ })
+ }
+
+ // Parent-Child Beziehungen herstellen
+ for (const group of groups) {
+ const node = map.get(group.id)
+
+ if (!node) {
+ continue
+ }
+
+ // Kein Parent => Root Node
+ if (!group.parentId) {
+ tree.push(node)
+ continue
+ }
+
+ const parent = map.get(group.parentId)
+
+ if (parent) {
+ parent.childs.push(node)
+ } else {
+ // Optional:
+ // Parent existiert nicht -> ebenfalls Root
+ tree.push(node)
+ }
+ }
+
+ return tree
+ }, [groups])
+
+ /**
+ * Prüft, ob eine Gruppe geöffnet ist.
+ */
+ const isOpen = useCallback(
+ (id: string) => {
+ return openGroups.has(id)
+ },
+ [openGroups]
+ )
+
+ /**
+ * Öffnet oder schließt eine Gruppe.
+ */
+ const toggle = useCallback((id: string) => {
+ setOpenGroups((current) => {
+ const next = new Set(current)
+
+ if (next.has(id)) {
+ next.delete(id)
+ } else {
+ next.add(id)
+ }
+
+ return next
+ })
+ }, [])
+
+ /**
+ * Öffnet eine Gruppe.
+ */
+ const open = useCallback((id: string) => {
+ setOpenGroups((current) => {
+ const next = new Set(current)
+
+ next.add(id)
+
+ return next
+ })
+ }, [])
+
+ /**
+ * Schließt eine Gruppe.
+ */
+ const close = useCallback((id: string) => {
+ setOpenGroups((current) => {
+ const next = new Set(current)
+
+ next.delete(id)
+
+ return next
+ })
+ }, [])
+
+ /**
+ * Schließt alle Gruppen.
+ */
+ const closeAll = useCallback(() => {
+ setOpenGroups(new Set())
+ }, [])
+
+ /**
+ * Öffnet alle Container.
+ */
+ const openAll = useCallback(() => {
+ const ids = new Set(
+ groups
+ .filter((group) => group.container)
+ .map((group) => group.id)
+ )
+
+ setOpenGroups(ids)
+ }, [groups])
+
+ // ----------------------------------------
+ // Selection
+ // ----------------------------------------
+
+ const select = useCallback((id: string|undefined) => {
+ id ? setSelectedGroupId(id) : setSelectedGroupId(null)
+ }, [])
+
+ const deselect = useCallback(() => {
+ setSelectedGroupId(null)
+ }, [])
+
+ const isSelected = useCallback(
+ (id: string) => {
+ return selectedGroupId === id
+ },
+ [selectedGroupId]
+ )
+
+ // ----------------------------------------
+ // Ausgewählten GroupResponse zurückgeben
+ // ----------------------------------------
+
+ const selectedGroup = useMemo(() => {
+ if (!selectedGroupId) {
+ return null
+ }
+
+ return (
+ groups.find((group) => group.id === selectedGroupId) ??
+ null
+ )
+ }, [groups, selectedGroupId])
+
+ return {
+ tree,
+ openGroups,
+ isOpen,
+ toggle,
+ open,
+ close,
+ openAll,
+ closeAll,
+ // Selection
+ selectedGroupId,
+ selectedGroup,
+ isSelected,
+ select,
+ deselect,
+ }
+}
\ No newline at end of file
diff --git a/src/hooks/rk2/useGroups.ts b/src/hooks/rk2/useGroups.ts
new file mode 100644
index 0000000..6f64aa8
--- /dev/null
+++ b/src/hooks/rk2/useGroups.ts
@@ -0,0 +1,71 @@
+
+
+import { GroupCreate, GroupDetail, GroupResponse, GroupTree } from "@/types/anlagen/groups";
+
+
+import { useCallback, useEffect, useMemo, useState } from "react";
+
+
+interface Props {
+ groups: GroupResponse[];
+}
+
+
+export function useGroups({ groups: initGroups }: Props) {
+
+
+ const [groupsExtended, setGroupsExteded] = useState([]);
+
+
+
+
+ useEffect(() => {
+ const gDeatial = initGroups.map((g) => ({ group: g, parent: g.parentId ? getGroupById(g.parentId) : undefined }))
+ setGroupsExteded(() => gDeatial);
+ }, [initGroups])
+
+
+
+ const getGroupById = useCallback((id: string) => {
+ return initGroups.find((g) => g.id === id);
+ }, [])
+
+ const getGroupPath = (group: GroupResponse) => {
+ const g: GroupResponse[] = []
+ let currentGroup = group;
+ g.push(group);
+ while (currentGroup) {
+
+
+ }
+ }
+
+ const getPathToRoot = (startGroupId?: string )=> {
+ if(!startGroupId) return [];
+ const groupMap = new Map(initGroups.map((group) => [group.id, group]));
+
+ const path: GroupResponse[] = []
+
+ let current = groupMap.get(startGroupId)
+
+ while (current) {
+ path.push(current)
+
+ if (!current.parentId) {
+ break
+ }
+
+ current = groupMap.get(current.parentId)
+ }
+
+ return path.reverse()
+ }
+
+
+ return {
+ getGroupById,
+ groupsExtended,
+ getPathToRoot
+
+ };
+}
\ No newline at end of file
diff --git a/src/hooks/rk2/useSignal.ts b/src/hooks/rk2/useSignal.ts
new file mode 100644
index 0000000..63c1e63
--- /dev/null
+++ b/src/hooks/rk2/useSignal.ts
@@ -0,0 +1,56 @@
+import { api_signalsByComp } from "@/actions/rk2/api-signals";
+import { ComponentResponse } from "@/types/anlagen/components";
+import { SignalResponse, SignalType } from "@/types/anlagen/signals";
+import { useEffect, useState } from "react";
+
+interface Props {
+ signal?: SignalResponse | undefined;
+ componenteId?: string
+
+}
+
+const emptySignal: Partial = {
+ anlageId: "",
+ id: "",
+ name: "",
+ type: SignalType.DIGITAL_INPUT,
+ componentId: "",
+ description: "",
+ ioId: ""
+}
+export function useSignal({ signal, componenteId }: Props) {
+ const [signalData, setSignalData] = useState>(emptySignal);
+
+ const [signalByComp, setSignalByComp] = useState([]);
+
+ useEffect(() => {
+ const loadCompSignal = async () => {
+ if (componenteId) {
+ let sigOfComp = await api_signalsByComp(componenteId)
+ setSignalByComp(()=>sigOfComp);
+ }
+ }
+ loadCompSignal();
+ }, [componenteId])
+
+ function setValue(fieldName: K, value: SignalResponse[K]): void {
+
+ setSignalData((cur) => ({ ...cur, [fieldName]: value }))
+ }
+
+ const string_to_signalType = (strType: string): SignalType => {
+
+ let signalT = Object.values(SignalType).find((s) => s === strType)
+ if (!signalT) signalT = SignalType.ANALOG_INPUT;
+ return signalT;
+ }
+
+ return {
+ setSignal: (signal: SignalResponse) => setSignalData(() => ({ ...signal })),
+ signalData,
+ setValue,
+ string_to_signalType,
+ signalByComp
+ // compType
+ };
+}
\ No newline at end of file
diff --git a/src/hooks/rk2/useUnit.ts b/src/hooks/rk2/useUnit.ts
new file mode 100644
index 0000000..fac94e1
--- /dev/null
+++ b/src/hooks/rk2/useUnit.ts
@@ -0,0 +1,63 @@
+import { UnitResponse, UnitType } from "@/types/anlagen/units";
+import { useEffect, useState } from "react";
+
+interface Props {
+ unit: UnitResponse | undefined;
+
+}
+const emptyUnit: UnitResponse = {
+ id:"",
+ name: "",
+ type: UnitType.BAND,
+ groupId: "",
+ bmk: "",
+ prefix: "",
+ postfix: "",
+ info: "",
+
+ components: [],
+}
+
+export function useUnit({ unit }: Props) {
+
+ const [data, setData] = useState(emptyUnit);
+
+ useEffect(() => {
+ if (unit) setData(() => ({...emptyUnit, ...unit }))
+ }, [unit])
+
+ const setValue = (fieldName: K, value: UnitResponse[K]): void => {
+ setData((cur) => ({ ...cur, [fieldName]: value }))
+ }
+
+
+
+
+
+ const unitType = Object.values(UnitType).map((t => ({ id: t, value: t })))
+
+ function isUnitType(value: string): value is UnitType {
+ return Object.values(UnitType).includes(value as UnitType);
+ }
+
+ function string_to_UnitType(value: string): UnitType {
+
+ if (!isUnitType(value)) {
+ throw new Error(`Ungültiger UnitType: ${value}`);
+ }
+ const unitType: UnitType = value;
+ return unitType
+ }
+
+ useEffect(() => {
+ }, [])
+
+ return {
+ unitData: data,
+ setValue,
+ emptyUnit,
+ unitType,
+ string_to_UnitType,
+
+ };
+}
\ No newline at end of file
diff --git a/src/hooks/rk2/useUnits.ts b/src/hooks/rk2/useUnits.ts
new file mode 100644
index 0000000..6bf1e4b
--- /dev/null
+++ b/src/hooks/rk2/useUnits.ts
@@ -0,0 +1,32 @@
+import { UnitResponse } from "@/types/anlagen/units";
+
+import { useEffect, useState } from "react";
+
+interface Props {
+ units: UnitResponse[];
+
+}
+
+export function useElements({ units: initUnits }: Props) {
+ const [selectedUnit, setSelectedUnit] = useState();
+
+ const [units, setUnits] = useState([])
+
+ useEffect(() => {
+ setUnits(() => [...orderByListAndNr(initUnits)])
+ }, [initUnits])
+
+ const orderByListAndNr = (units: UnitResponse[]): UnitResponse[] => {
+ return units.sort((unit1, unit2) => {
+ const nameCompare = unit1.name.localeCompare(unit2.name);
+ return nameCompare ;
+ })
+ }
+
+
+ return {
+ units,
+ selectedUnit: selectedUnit,
+ setSelectedUnnit: setSelectedUnit
+ };
+}
\ No newline at end of file
diff --git a/src/hooks/useElement.ts b/src/hooks/useElement.ts
new file mode 100644
index 0000000..39bf918
--- /dev/null
+++ b/src/hooks/useElement.ts
@@ -0,0 +1,22 @@
+import { Element, ExcelProCon } from "@/types/rk";
+import { useEffect, useState } from "react";
+
+interface Props {
+ element: Element|undefined;
+
+}
+
+export function useElement({ element }: Props) {
+ const [selectedElement, setSelectedElement] = useState();
+ const [proConExcelLists, setProConExcelLists] = useState<{ id: string, value: string }[]>([])
+ const [elements, setElements] = useState([])
+
+ useEffect(() => {
+ let listen = Object.values(ExcelProCon).map((eList,index) => ({id:eList,value:eList}))
+ setProConExcelLists(()=>listen);
+ },[])
+
+ return {
+ proConExcelLists
+ };
+}
\ No newline at end of file
diff --git a/src/hooks/useElements.ts b/src/hooks/useElements.ts
new file mode 100644
index 0000000..246784d
--- /dev/null
+++ b/src/hooks/useElements.ts
@@ -0,0 +1,49 @@
+import { Element, ExcelProCon } from "@/types/rk";
+import { useEffect, useState } from "react";
+
+interface Props {
+ elements: Element[];
+
+}
+
+export function useElements({ elements: initElements }: Props) {
+ const [selectedElement, setSelectedElement] = useState();
+ const [filterExcelListe, setFilterExcelListe] = useState(ExcelProCon.Unkonwon)
+ const [elements, setElements] = useState([])
+
+ useEffect(() => {
+
+ setElements(() => [...filterByList(orderByListAndNr(initElements))])
+ }, [filterExcelListe,initElements])
+
+ const orderByListAndNr = (elements: Element[]): Element[] => {
+ return elements.sort((ele1, ele2) => {
+ const nameCompare = ele1.excelListe.localeCompare(ele2.excelListe);
+ // wenn liste Name != listenvergleich zurückgeben ansonsten nr vergleichen
+ return nameCompare !== 0 ? nameCompare : ele1.excelNr - ele2.excelNr; // wenn name gleich → nach number
+ })
+ }
+
+ const filterByList = (elements: Element[]): Element[] => {
+ return elements.filter((ele) => {
+ let comp = ''
+ if (filterExcelListe == ExcelProCon.Aggregate) comp = ExcelProCon.Aggregate;
+ if (filterExcelListe == ExcelProCon.SensorenAnalog) comp = 'Sensoren';
+ if (filterExcelListe == ExcelProCon.SensorenDigital) comp = 'Sensoren';
+ if (filterExcelListe == ExcelProCon.AnzeigeVisu) comp = ExcelProCon.AnzeigeVisu;
+ if (filterExcelListe == ExcelProCon.Texte) comp = ExcelProCon.Texte;
+ if (comp == '') return true;
+
+ return comp == ele.excelListe;
+
+
+ })
+ }
+ return {
+ elements,
+ selectedElement,
+ filterExcelListe,
+ setFilterExcelListe,
+ setSelectedItem: setSelectedElement
+ };
+}
\ No newline at end of file
diff --git a/src/hooks/useNumberInput.ts b/src/hooks/useNumberInput.ts
new file mode 100644
index 0000000..db39135
--- /dev/null
+++ b/src/hooks/useNumberInput.ts
@@ -0,0 +1,48 @@
+import { useEffect, useState } from "react";
+
+interface UseNumberInputProps {
+ value: number | undefined;
+ onCommit: (value: number) => void;
+ min?: number;
+ max?: number;
+ fallback?: number; // wenn leer / invalid
+}
+
+export function useNumberInput({value,onCommit,min,max,fallback = 0}: UseNumberInputProps) {
+ const [input, setInput] = useState( value !== undefined ? String(value) : "");
+
+ // sync wenn sich value von außen ändert
+ useEffect(() => {
+ setInput(value !== undefined ? String(value) : "");
+ }, [value]);
+
+ const handleChange = (val: string) => {
+ setInput(val);
+ };
+
+ const handleKeybord = (e:React.KeyboardEvent) => {
+ if(e.key==='Enter') handleBlur();
+ };
+
+ const handleBlur = () => {
+
+ let parsed = input ==='0' ? 0 : parseFloat(input);
+
+ if (isNaN(parsed)) {
+ parsed = fallback;
+ }
+
+ if (min !== undefined) parsed = Math.max(min, parsed);
+ if (max !== undefined) parsed = Math.min(max, parsed);
+
+ onCommit(parsed);
+ setInput(String(parsed));
+ };
+
+ return {
+ value: input,
+ onChange: (e: React.ChangeEvent) => handleChange(e.target.value),
+ onKeyUp: (e: React.KeyboardEvent) => handleKeybord(e),
+ onBlur: handleBlur,
+ };
+}
\ No newline at end of file
diff --git a/src/hooks/useStringInput.ts b/src/hooks/useStringInput.ts
new file mode 100644
index 0000000..2a65a6e
--- /dev/null
+++ b/src/hooks/useStringInput.ts
@@ -0,0 +1,55 @@
+import { useEffect, useState } from "react";
+
+interface UseStringInputProps {
+ value: string | undefined;
+ onCommit: (value: string) => void;
+
+ fallback?: string; // wenn leer / invalid
+}
+
+export function useStringInput({ value, onCommit, fallback = '' }: UseStringInputProps) {
+
+ const [orginal, setOrginal] = useState("");
+ const [input, setInput] = useState(value !== undefined ? String(value) : "");
+ const [isDirty, setDirty] = useState(false);
+
+ // sync wenn sich value von außen ändert
+ useEffect(() => {
+ if (!value) return;
+ setDirty(false);
+ setOrginal(value);
+ setInput(value !== undefined ? String(value) : "");
+ }, [value]);
+
+ const handleChange = (val: string) => {
+ setInput(val);
+ if(val!==orginal) setDirty(true);
+ };
+
+ const handleKeybord = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') handleBlur();
+ if (e.key === 'Escape') {
+ setInput(orginal);
+ setDirty(false);
+ }
+ };
+
+ const handleBlur = () => {
+
+ if (isDirty) {
+ let parsed = input;
+
+ setOrginal(parsed);
+ setDirty(false);
+ onCommit(parsed);
+
+ }
+ };
+
+ return {
+ value: input,
+ onChange: (e: React.ChangeEvent) => handleChange(e.target.value),
+ onKeyUp: (e: React.KeyboardEvent) => handleKeybord(e),
+ onBlur: handleBlur,
+ };
+}
\ No newline at end of file
diff --git a/src/hooks/useSvgDrag.ts b/src/hooks/useSvgDrag.ts
new file mode 100644
index 0000000..4f16cad
--- /dev/null
+++ b/src/hooks/useSvgDrag.ts
@@ -0,0 +1,272 @@
+import { useRef, useEffect } from "react";
+import { VisuElement } from "@/types/visu/visuElements.types";
+import { useVisuElementContext } from "@/_context/visu/useVisuElementsContext";
+import { snapPointToGrid } from "@/components/visu/utilities/snap";
+import { api_VisuElementUpdate } from "@/actions/rk/api-visuElements";
+
+interface UseSvgDragProps {
+ svgRef: any//React.RefObject;
+ gridSize: number;
+
+ setViewBox: React.Dispatch<
+ React.SetStateAction<{ x: number; y: number; w: number; h: number }>
+ >;
+}
+
+export function useSvgDrag({ svgRef, gridSize, setViewBox, }: UseSvgDragProps) {
+
+ const { state, dispatch } = useVisuElementContext();
+ const stateRef = useRef(state);
+ const gridRef = useRef(gridSize);
+ const dragRef = useRef({
+ lastX: 0,
+ lastY: 0,
+ });
+
+
+
+ const ctmRef = useRef(null);
+
+ const frame = useRef(null);
+ const isSpacePressed = useRef(false);
+
+ // -----------------------
+ // ⌨️ SPACE TRACKING
+ // -----------------------
+ useEffect(() => {
+ const down = (e: KeyboardEvent) => {
+ if (e.code === "Space") isSpacePressed.current = true;
+ };
+ const up = (e: KeyboardEvent) => {
+ if (e.code === "Space") isSpacePressed.current = false;
+ };
+
+ window.addEventListener("keydown", down);
+ window.addEventListener("keyup", up);
+
+ return () => {
+ window.removeEventListener("keydown", down);
+ window.removeEventListener("keyup", up);
+ };
+ }, []);
+
+ useEffect(() => {
+ stateRef.current = state;
+ }, [state]);
+
+ useEffect(() => {
+ gridRef.current = gridSize;
+ }, [gridSize]);
+
+ const screenToSvg = (x: number, y: number) => {
+ const svg = svgRef.current!;
+ if (!svg || !ctmRef.current) return { x: 0, y: 0 };
+
+ const pt = svg.createSVGPoint();
+ pt.x = x;
+ pt.y = y;
+
+ return pt.matrixTransform(ctmRef.current);
+ };
+
+
+ //---
+ // startElementDrag
+ //---
+ const startElementDrag = (element: VisuElement, eventMouseDown: React.MouseEvent) => {
+ eventMouseDown.stopPropagation();
+
+ const svg = svgRef.current!;
+ const ctm = svg.getScreenCTM();
+ if (!ctm) return;
+
+ ctmRef.current = ctm.inverse();
+
+ const pt = svg.createSVGPoint();
+ pt.x = eventMouseDown.clientX;
+ pt.y = eventMouseDown.clientY;
+
+ const pos = pt.matrixTransform(ctmRef.current);
+
+ dragRef.current.lastX = pos.x;
+ dragRef.current.lastY = pos.y;
+
+
+ dispatch({ type: "SELECT_ELEMENT", payload: element, });
+
+ dispatch({
+ type: "DRAG_START",
+ payload: {
+ mode: "element",
+ elementId: element.id,
+ x: pos.x,
+ y: pos.y,
+ },
+ });
+ };
+
+ //---
+ // startCanvasDrag
+ //---
+ const startCanvasDrag = (e: React.MouseEvent) => {
+ const svg = svgRef.current!;
+ if (!svg) return;
+
+ const ctm = svg.getScreenCTM();
+ if (!ctm) return;
+
+ ctmRef.current = ctm.inverse();
+
+ const pt = svg.createSVGPoint();
+ pt.x = e.clientX;
+ pt.y = e.clientY;
+
+ const pos = pt.matrixTransform(ctmRef.current);
+
+ dragRef.current.lastX = pos.x;
+ dragRef.current.lastY = pos.y;
+
+ dispatch({ type: "DESELECT_ELEMENT" });
+
+ dispatch({
+ type: "DRAG_START",
+ payload: {
+ mode: "canvas",
+ x: pos.x,
+ y: pos.y,
+ },
+ });
+ };
+
+
+ useEffect(() => {
+
+ const move = (e: MouseEvent) => {
+ if (frame.current) return;
+
+ frame.current = requestAnimationFrame(() => {
+ frame.current = null;
+ if (!ctmRef.current) return;
+
+ const pos = screenToSvg(e.clientX, e.clientY);
+ const currentState = stateRef.current;
+ const drag = currentState.drag;
+
+ if (!drag || drag.mode === "idle") return;
+
+ const dx = pos.x - dragRef.current.lastX;
+ const dy = pos.y - dragRef.current.lastY;
+
+ // 🔥 CANVAS MOVE
+ if (drag.mode === "canvas") {
+ setViewBox((prev) => ({
+ x: prev.x - dx,
+ y: prev.y - dy,
+ w: prev.w,
+ h: prev.h,
+ }));
+ }
+
+ // update ref (NICHT state)
+ dragRef.current.lastX = pos.x;
+ dragRef.current.lastY = pos.y;
+
+ if (drag.mode === "element") {
+ dispatch({
+ type: "DRAG_MOVE",
+ payload: { x: pos.x, y: pos.y },
+ });
+ }
+ });
+ };
+
+ const up = async () => {
+ ctmRef.current = null;
+ const currentState = stateRef.current;
+ const currentGridSize = gridRef.current;
+
+ console.log('up1', currentState.drag.mode, currentState.selectedElement?.id)
+ if (currentState.drag.mode === "element" && currentState.selectedElement) {
+ const element = currentState.elements.find(
+ (el) => el.id === currentState.drag.elementId
+ );
+
+
+ if (!element) return;
+
+
+ const snapped = snapPointToGrid(
+ element.posX,
+ element.posY,
+ currentGridSize
+ );
+
+ const updated = {
+ ...element,
+ posX: snapped.x,
+ posY: snapped.y,
+ };
+ dispatch({ type: "UPDATE_ELEMENT", payload: updated });
+ //dispatch({ type: "SELECT_ELEMENT", payload: updated });
+ await api_VisuElementUpdate(element.id, updated);
+ }
+
+ dispatch({ type: "DRAG_END" });
+ };
+
+ window.addEventListener("mousemove", move);
+ window.addEventListener("mouseup", up);
+
+ return () => {
+ window.removeEventListener("mousemove", move);
+ window.removeEventListener("mouseup", up);
+ };
+ }, []);
+
+
+
+
+ const handleWheel = (e: React.WheelEvent) => {
+ if (!isSpacePressed.current) return;
+
+ e.preventDefault();
+
+ const svg = svgRef.current;
+ if (!svg) return;
+
+ const scaleFactor = 1.1;
+ const zoom = e.deltaY < 0 ? 1 / scaleFactor : scaleFactor;
+
+ const pt = svg.createSVGPoint();
+ pt.x = e.clientX;
+ pt.y = e.clientY;
+
+ const ctm = svg.getScreenCTM();
+ if (!ctm) return;
+
+ const cursor = pt.matrixTransform(ctm.inverse());
+
+ setViewBox((prev) => {
+ const newW = prev.w * zoom;
+ const newH = prev.h * zoom;
+
+ const newX = cursor.x - (cursor.x - prev.x) * (newW / prev.w);
+ const newY = cursor.y - (cursor.y - prev.y) * (newH / prev.h);
+
+ return {
+ x: newX,
+ y: newY,
+ w: newW,
+ h: newH,
+ };
+ });
+ };
+
+
+
+ return {
+ startCanvasDrag,
+ startElementDrag,
+ handleWheel
+ };
+}
\ No newline at end of file
diff --git a/src/hooks/useSvgKey.ts b/src/hooks/useSvgKey.ts
new file mode 100644
index 0000000..9139137
--- /dev/null
+++ b/src/hooks/useSvgKey.ts
@@ -0,0 +1,123 @@
+import { useRef, useEffect, RefObject } from "react";
+import { VisuElement } from "@/types/visu/visuElements.types";
+import { useVisuElementContext } from "@/_context/visu/useVisuElementsContext";
+
+interface UseSvgKeyProps {
+ stateObj: RefObject<{
+ type: "canvas" | "element" | "selected" | null;
+ element?: VisuElement;
+ lastX: number;
+ lastY: number;
+ startX: number;
+ startY: number;
+ ctm?: DOMMatrix;
+ }>
+}
+
+interface Props{
+ enabled:boolean
+}
+
+export function useSvgKey({ enabled }:Props) {
+
+
+ const { state, dispatch } = useVisuElementContext()
+
+
+
+ useEffect(() => {
+
+
+ const handle = (event: KeyboardEvent) => {
+ if(!enabled) return;
+ const step = event.shiftKey ? 10 : 1;
+
+
+ let dx = 0;
+ let dy = 0;
+
+ switch (event.key) {
+ case "Escape":
+ dispatch({ type: "DESELECT_ELEMENT" })
+ return;
+ case "ArrowRight":
+ dx = step;
+ break;
+ case "ArrowLeft":
+ dx = -step;
+ break;
+ case "ArrowDown":
+ dy = step;
+ break;
+ case "ArrowUp":
+ dy = -step;
+ break;
+ default:
+ return;
+ }
+ if (dx > 0 || dy > 0) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
+ dispatch({
+ type: "MOVE_SELECTED_ELEMENT",
+ payload: { dx, dy },
+ });
+
+
+ }
+
+
+
+ window.addEventListener("keydown", handle);
+
+ return () => {
+ window.removeEventListener("keydown", handle);
+ };
+ }, [state, dispatch]);
+
+ const onKeyDown = (event: React.KeyboardEvent) => {
+ console.log('SVG Key Down');
+ const step = event.shiftKey ? 10 : 1;
+
+
+ let dx = 0;
+ let dy = 0;
+
+ switch (event.key) {
+ case "Escape":
+ dispatch({ type: "DESELECT_ELEMENT" })
+ return;
+ case "ArrowRight":
+ dx = step;
+ break;
+ case "ArrowLeft":
+ dx = -step;
+ break;
+ case "ArrowDown":
+ dy = step;
+ break;
+ case "ArrowUp":
+ dy = -step;
+ break;
+ default:
+ return;
+ }
+ if (dx > 0 || dy > 0) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
+ dispatch({
+ type: "MOVE_SELECTED_ELEMENT",
+ payload: { dx, dy },
+ });
+
+
+ }
+
+ return {
+ onKeyDown
+ };
+}
\ No newline at end of file
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
new file mode 100644
index 0000000..daab5de
--- /dev/null
+++ b/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
\ No newline at end of file
diff --git a/src/types/anlagen/components.ts b/src/types/anlagen/components.ts
new file mode 100644
index 0000000..71076ef
--- /dev/null
+++ b/src/types/anlagen/components.ts
@@ -0,0 +1,83 @@
+import { SignalCreate, SignalType } from "./signals";
+import { UnitType } from "./units"
+
+
+export type ComponentType = string
+
+//--- Date Componente from/to Backend
+
+export interface ComponentResponse {
+ id: string
+ name: string,
+ prefix?: string;
+ postfix?: string;
+ index?:number;
+ type: ComponentType,
+ properties?: Record,
+}
+
+export interface ComponentOfUnit {
+ unitId:string,
+ component:ComponentResponse
+}
+
+export interface ComponentUpdate {
+ name?: string,
+ prefix?: string;
+ postfix?: string;
+ index?:number;
+ type?: ComponentType,
+ properties?: Record,
+
+}
+
+export interface ComponentCreate {
+ name: string,
+ prefix?: string;
+ postfix?: string;
+ index?:number;
+ type: ComponentType,
+ properties?: Record,
+}
+
+
+//--- Properties
+
+export type PropertyType =
+ | "text"
+ | "number"
+ | "boolean"
+ | "select"
+
+export interface PropertyOption {
+ value: string
+ label: string
+}
+
+export interface PropertyDefinition {
+ key: string
+ label: string
+ type: PropertyType
+
+ required?: boolean
+ defaultValue: unknown
+
+ options?: PropertyOption[]
+}
+
+export interface ComponentTypeDefinition {
+ type: ComponentType
+ label: string
+
+ properties: PropertyDefinition[]
+}
+
+export interface ComponentDefaults {
+ type: ComponentType
+ name: string
+ signals: {
+ name: string,
+ type: SignalType,
+ }[]
+
+}
diff --git a/src/types/anlagen/groups.ts b/src/types/anlagen/groups.ts
new file mode 100644
index 0000000..3b57229
--- /dev/null
+++ b/src/types/anlagen/groups.ts
@@ -0,0 +1,35 @@
+export interface GroupResponse {
+ id: string
+ name: string
+ nr: string
+ container: boolean
+ parentId?: string
+}
+
+export interface GroupCreate {
+ id?: string
+ name: string
+ nr: string
+ container: boolean
+ parentId?: string
+}
+
+export interface GroupUpdate {
+ name?: string;
+ nr?: string;
+ container?: boolean
+ parentId?: string
+}
+
+
+// Tree View
+export interface GroupTree {
+ group: GroupResponse,
+ childs: GroupTree[],
+ //parent?:GroupResponse
+}
+
+export interface GroupDetail {
+ group: GroupResponse,
+ parent?: GroupResponse
+}
diff --git a/src/types/anlagen/ios.ts b/src/types/anlagen/ios.ts
new file mode 100644
index 0000000..1086f84
--- /dev/null
+++ b/src/types/anlagen/ios.ts
@@ -0,0 +1,64 @@
+
+ export enum IOType {
+ DIGITAL_INPUT = 'DigitalInput',
+ DIGITAL_OUTPUT = 'DigitalOutput',
+ ANALOG_INPUT = 'AnalogInput',
+ ANALOG_OUTPUT = 'AnalogOutput',
+}
+
+export interface IoResponse {
+ id:string,
+ anlageId: string,
+
+ type: IOType,
+ address: number,
+ adrBit?: number,
+ plcId?: string,
+ moduleId?: string,
+ channel?: number,
+ text?: string,
+ seite?: number,
+ seiteName?: string,
+ excelData?: string,
+ plcData?: string,
+}
+
+export interface IoCreate {
+ //anlageId: string,
+ type: IOType,
+ address: number,
+ adrBit?: number,
+ plcId?: string,
+ moduleId?: string,
+ channel?: number,
+ text?: string,
+ seite?: number,
+ seiteName?: string,
+ excelData?: string,
+ plcData?: string,
+}
+
+export interface IoUpdate {
+ anlageId?: string,
+ type?: IOType,
+ address?: number,
+ adrBit?: number,
+ plcId?: string,
+ moduleId?: string,
+ channel?: number,
+ text?: string,
+ seite?: number,
+ seiteName?: string,
+ excelData?: string,
+ plcData?: string,
+}
+
+//--- n.v.
+export interface Plc {
+
+ name: string,
+ type: string,
+ ipAddress?: string,
+ description?: string,
+}
+
diff --git a/src/types/anlagen/signals.ts b/src/types/anlagen/signals.ts
new file mode 100644
index 0000000..c094498
--- /dev/null
+++ b/src/types/anlagen/signals.ts
@@ -0,0 +1,37 @@
+
+export enum SignalType {
+ DIGITAL_INPUT = 'DigitalInput',
+ DIGITAL_OUTPUT = 'DigitalOutput',
+ ANALOG_INPUT = 'AnalogInput',
+ ANALOG_OUTPUT = 'AnalogOutput',
+}
+
+export interface SignalResponse {
+ id: string,
+ anlageId: string,
+
+ name: string,
+ type: SignalType,
+ description?: string,
+ ioId?: string,
+ componentId?: string,
+}
+
+
+export interface SignalCreate {
+ // anlageId?: string,
+ componentId?: string,
+ name: string,
+ type: SignalType,
+ description?: string,
+ ioId?: string,
+}
+
+export interface SignalUpdate {
+ anlageId: string,
+ name: string,
+ type: SignalType,
+ description?: string,
+ ioId?: string,
+ componentId?: string,
+}
diff --git a/src/types/anlagen/units.ts b/src/types/anlagen/units.ts
new file mode 100644
index 0000000..963ae08
--- /dev/null
+++ b/src/types/anlagen/units.ts
@@ -0,0 +1,61 @@
+import { ComponentResponse } from "./components";
+
+export enum UnitType {
+ BAND = 'Band',
+ BRECHER = 'Brecher',
+ WEICHE = 'Weiche',
+ SIEB = 'Sieb',
+ ANTRIEB = 'Antrieb',
+}
+
+//--- Unit
+
+export interface UnitResponse {
+ id: string,
+ name: string,
+ anlageId?: string,
+ bmk?: string,
+ prefix?: string,
+ postfix?: string,
+ info?: string,
+ type: UnitType,
+ groupId?: string,
+ components: ComponentResponse[],
+}
+
+export interface UnitCreate {
+ id?:string,
+ name: string,
+ anlageId?: string,
+ bmk?: string,
+ prefix?: string,
+ postfix?: string,
+ info?: string,
+ type: UnitType,
+ groupId?: string,
+
+}
+
+
+export interface UnitUpdateBase {
+ name?: string,
+ type?: UnitType,
+ groupId?: string,
+ anlageId?: string,
+ bmk?: string,
+ prefix?: string,
+ postfix?: string,
+ info?: string,
+
+}
+
+/*
+export interface UnitUpdateComponent {
+ name?: string,
+ prefix?: string;
+ postfix?: string;
+ index?:number;
+ type?: UnitType,
+ groupId?: string,
+}
+*/
diff --git a/src/types/api.types.ts b/src/types/api.types.ts
new file mode 100644
index 0000000..c86c79d
--- /dev/null
+++ b/src/types/api.types.ts
@@ -0,0 +1,8 @@
+type ValidationError = {
+
+ errors: string[];
+};
+
+type ActionResult =
+ | { success: true,data:K }
+ | { success: false; errors: string[],data?:any };
diff --git a/src/types/draw/draw.path.types.ts b/src/types/draw/draw.path.types.ts
new file mode 100644
index 0000000..1a7250d
--- /dev/null
+++ b/src/types/draw/draw.path.types.ts
@@ -0,0 +1,103 @@
+
+/** Basis-Typ: alle Commands haben ein Flag ob relativ oder absolut */
+export interface PathBaseCommand {
+ /** true = relativer Befehl (kleiner Buchstabe), false/undefined = absolut */
+ relative?: boolean;
+}
+
+/** Move-To */
+export interface MoveToCommand extends PathBaseCommand {
+ type: "M";
+ x: number;
+ y: number;
+}
+
+/** Line-To */
+export interface LineToCommand extends PathBaseCommand {
+ type: "L";
+ x: number;
+ y: number;
+}
+
+/** Horizontal Line-To */
+export interface HorizontalLineToCommand extends PathBaseCommand {
+ type: "H";
+ x: number;
+}
+
+/** Vertical Line-To */
+export interface VerticalLineToCommand extends PathBaseCommand {
+ type: "V";
+ y: number;
+}
+
+/** Cubic Bézier Curve */
+export interface CubicBezierCommand extends PathBaseCommand {
+ type: "C";
+ x1: number; y1: number;
+ x2: number; y2: number;
+ x: number; y: number;
+}
+
+/** Smooth Cubic Bézier Curve */
+export interface SmoothCubicBezierCommand extends PathBaseCommand {
+ type: "S";
+ x2: number; y2: number;
+ x: number; y: number;
+}
+
+/** Quadratic Bézier Curve */
+export interface QuadraticBezierCommand extends PathBaseCommand {
+ type: "Q";
+ x1: number; y1: number;
+ x: number; y: number;
+}
+
+/** Smooth Quadratic Bézier Curve */
+export interface SmoothQuadraticBezierCommand extends PathBaseCommand {
+ type: "T";
+ x: number; y: number;
+}
+
+/** Elliptical Arc */
+export interface ArcCommand extends PathBaseCommand {
+ type: "A";
+ rx: number;
+ ry: number;
+ xAxisRotation: number;
+ largeArcFlag: 0 | 1;
+ sweepFlag: 0 | 1;
+ x: number;
+ y: number;
+}
+
+/** Close Path */
+export interface ClosePathCommand extends PathBaseCommand {
+ type: "Z";
+}
+
+/** Union aller Path-Kommandos */
+export type PathCommand =
+ | MoveToCommand
+ | LineToCommand
+ | HorizontalLineToCommand
+ | VerticalLineToCommand
+ | CubicBezierCommand
+ | SmoothCubicBezierCommand
+ | QuadraticBezierCommand
+ | SmoothQuadraticBezierCommand
+ | ArcCommand
+ | ClosePathCommand;
+
+/** Ein ganzer Pfad ist einfach eine Liste von Kommandos */
+export type SvgPath = PathCommand[];
+
+
+
+
+interface PropDimPath {
+ cx: number
+ cy: number
+ rx: number
+ ry: number
+}
diff --git a/src/types/draw/draw.types.ts b/src/types/draw/draw.types.ts
new file mode 100644
index 0000000..669d8be
--- /dev/null
+++ b/src/types/draw/draw.types.ts
@@ -0,0 +1,12 @@
+export interface DrawShapeData {
+ name:string;
+ svg:string;
+ height: number;
+ length:number;
+
+}
+
+export interface DrawShapeOptionen{
+ scale?:number;
+ raster?:number;
+}
\ No newline at end of file
diff --git a/src/types/procon/aggregate.ts b/src/types/procon/aggregate.ts
new file mode 100644
index 0000000..9493bc3
--- /dev/null
+++ b/src/types/procon/aggregate.ts
@@ -0,0 +1,34 @@
+import { ProConView } from "./views";
+
+
+export interface ProConAggregate {
+ Id: number
+ SPSAntriebId: number,
+ Bezeichnung: string,
+ Bemerkung: string,
+ AntriebPositionSchaltplan: string,
+ Referenznummer: string,
+ Info: string,
+ AnzahlMotor: number,
+ AnsteuerungMotor: number,
+ Drehzahlwaechter: number,
+ Funktion: number,
+ Getriebeuebersetzung: number,
+ NenndrehzahlMotor: number,
+ MitVisuSlider: boolean,
+ BMK: string,
+ ViewName: string,
+}
+
+
+export interface ProConAggVisu {
+ anlage: string;
+ aggNr: number;
+ bezeichnung: string;
+ viewName: string;
+ bmk: string;
+ anzMotoren: number;
+ dzUeVorhanden: boolean;
+ visuElements:ProConView[],
+
+}
diff --git a/src/types/procon/anzeigen.ts b/src/types/procon/anzeigen.ts
new file mode 100644
index 0000000..2e59390
--- /dev/null
+++ b/src/types/procon/anzeigen.ts
@@ -0,0 +1,40 @@
+
+export interface ProConAnzeige {
+ Id: number,
+ DatenNummer: number,
+ Text: string,
+ Skalierung: number,
+ Einheit: string,
+ Max: number,
+ Min: number,
+ StatistikTyp: number,
+
+}
+
+
+export interface ProConAnzeigeUpdate {
+ DatenNummer?: number,
+ Text?: string,
+ Skalierung?: number,
+ Einheit?: string,
+ Max?: number,
+ Min?: number,
+ StatistikTyp?: number,
+
+}
+
+
+export interface ProConAnzeigeVisuElement {
+ Id: number,
+ Height: number,
+ Width: number,
+ PositionX: number,
+ PositionY: number,
+ PositionZ: number,
+ Show: boolean,
+ Discriminator: string,
+
+ AnzeigeParameterId: number | undefined,
+ State: number | undefined,
+ ViewId: string | undefined,
+}
diff --git a/src/types/procon/index.ts b/src/types/procon/index.ts
new file mode 100644
index 0000000..944ac3a
--- /dev/null
+++ b/src/types/procon/index.ts
@@ -0,0 +1,4 @@
+export * from './aggregate';
+export * from './komponenten';
+export * from './views';
+export * from './visualisierungen';
\ No newline at end of file
diff --git a/src/types/procon/komponenten.ts b/src/types/procon/komponenten.ts
new file mode 100644
index 0000000..8c9b843
--- /dev/null
+++ b/src/types/procon/komponenten.ts
@@ -0,0 +1,9 @@
+
+
+export interface ProConKomponenten {
+ Id: number
+ Status: number
+ ViewId: string
+ XamlVisualisierungId: number
+}
+
diff --git a/src/types/procon/meldungen.ts b/src/types/procon/meldungen.ts
new file mode 100644
index 0000000..64d594c
--- /dev/null
+++ b/src/types/procon/meldungen.ts
@@ -0,0 +1,29 @@
+
+export interface ProConMeldung {
+ Id: number,
+ Bezeichnung: string,
+ Relevant: boolean,
+ AggregateId: number,
+ Typ: number,
+ Deaktiviert: boolean,
+ SelbstQuittierend: boolean,
+ BenutzerId: number,
+ GeaendertAm: Date,
+ IstWarnung: boolean,
+}
+
+
+
+export interface ProConAnzeigeUpdate {
+ Bezeichnung?: string,
+ Relevant?: boolean,
+ AggregateId?: number,
+ Typ?: number,
+ Deaktiviert?: boolean,
+ SelbstQuittierend?: boolean,
+ BenutzerId?: number,
+ GeaendertAm?: Date,
+ IstWarnung?: boolean,
+
+}
+
diff --git a/src/types/procon/sensoren.ts b/src/types/procon/sensoren.ts
new file mode 100644
index 0000000..9c45eba
--- /dev/null
+++ b/src/types/procon/sensoren.ts
@@ -0,0 +1,76 @@
+export class ProConSensor {
+ Id!: number;
+ Typ!: number;
+ DatenNummer!: number;
+ Text!: string;
+ Einheit!: string;
+ Min!: number;
+ Max!: number;
+ StatistikTyp!: number;
+
+}
+
+
+export class ProConSensorenUpdate {
+ Typ?: number;
+ DatenNummer?: number;
+ Text?: string;
+ Einheit?: string;
+ Min?: number;
+ Max?: number;
+ StatistikTyp?: number;
+
+}
+
+
+export class ProConSensorVisuElement {
+ Id!: number;
+ Width!: number;
+ Height!: number;
+ PositionX!: number;
+ PositionY!: number;
+ PositionZ!: number;
+ Show!: boolean;
+ Discriminator!: string;
+ MainView!: boolean;
+ SensorId!: number;
+ SensorVisualisierung_State!: number;
+ SensorVisualisierung_ViewId!: number;
+}
+
+/*
+ Analog
+ SensorVisualisierung_State
+ 2 Hauptview
+ Einheit = % -> Kasten mit Prozent
+ sonst -> Text
+ 3 Min
+ 4 Min_Min
+ 5 Max
+ 6 Max_Max
+ Digital
+ SensorVisualisierung_State
+ 0 Ein!
+ 1 Aus
+
+ 0 Anlage Aus
+ 1 Bediener Startet Anlage
+ 2 Anlage läuft hoch
+ 3 Anlage läuft Austrag nicht Freigegeben
+ 4 Anlage Produziert ohne Material
+ 5 Anlage Produziert
+ 6 Anlage läuft leer
+ Zustand alle Aggregate aus
+ --> 0
+
+ Anlage Störung
+ --> Stopp Aggregate von Aufgabe bis zur Störung
+ Störung behoben und Quittiert
+ --> 1
+
+ Anlage Störung: Manueller eingriff (Not-Halt, Reißleine, Reparaturschalter, Vorortbedienung ein, im Handbetrieb/Revisionsbetrieb wurde ein Aggregat abgeschalten)
+ Eingriff beendet und Quittiert
+ --> 1
+
+
+*/
\ No newline at end of file
diff --git a/src/types/procon/texte.ts b/src/types/procon/texte.ts
new file mode 100644
index 0000000..039c919
--- /dev/null
+++ b/src/types/procon/texte.ts
@@ -0,0 +1,34 @@
+export interface ProConText {
+ Id: number,
+ Height: number,
+ PositionX: number,
+ PositionY: number,
+ Width: number,
+ Show: boolean,
+ Discriminator: string,
+ PositionZ: number,
+ FontFamily: string,
+ FontSize: number,
+ FontWeight: number,
+ Hintergrund: string,
+ Nummer: number,
+ TexteVisualisierung_Text: string,
+ TextFarbe: string,
+}
+
+export interface ProConTextUpdate {
+ Height?: number,
+ PositionX?: number,
+ PositionY?: number,
+ Width?: number,
+ Show?: boolean,
+ Discriminator?: string,
+ PositionZ?: number,
+ FontFamily?: string,
+ FontSize?: number,
+ FontWeight?: number,
+ Hintergrund?: string,
+ Nummer?: number,
+ TexteVisualisierung_Text?: string,
+ TextFarbe?: string,
+}
\ No newline at end of file
diff --git a/src/types/procon/views.ts b/src/types/procon/views.ts
new file mode 100644
index 0000000..602f25f
--- /dev/null
+++ b/src/types/procon/views.ts
@@ -0,0 +1,24 @@
+
+export interface ProConDBViews {
+ Id: string,
+ Data: string
+}
+
+export class ProConXamlData {
+ state!: string;
+ id!: string;
+ xaml!: string;
+}
+
+export interface ProConView {
+ id: number,
+ viewType:string,
+ viewIndex:number,
+ width: number;
+ height: number;
+ posX: number;
+ posY: number;
+ zIndex: number;
+ show: boolean;
+ views: ProConXamlData[]
+}
\ No newline at end of file
diff --git a/src/types/procon/visualisierungen.ts b/src/types/procon/visualisierungen.ts
new file mode 100644
index 0000000..44b6f80
--- /dev/null
+++ b/src/types/procon/visualisierungen.ts
@@ -0,0 +1,67 @@
+import { ProConAggregate } from "./aggregate";
+
+
+export interface ProConVisualisierungen {
+ Id: number,
+ Height: number,
+ PositionX: number,
+ PositionY: number,
+ Width: number,
+ Show: boolean,
+ Discriminator: string,
+ Text: string | undefined,
+ MainView: true,
+ AggregateId: number,
+ Number: number | undefined,
+ PositionZ: number,
+ SteuerungId: number | undefined,
+ AnzeigeParameterId: number | undefined,
+ State: number | undefined,
+ ViewId: string | undefined,
+ ZuweisungGauge: number | undefined,
+ SensorVisualisierung_State: number | undefined,
+ SensorVisualisierung_ViewId: number | undefined,
+ SensorId: number | undefined,
+ FontFamily: string | undefined,
+ FontSize: number | undefined,
+ FontWeight: number | undefined,
+ Hintergrund: string | undefined,
+ Nummer: number | undefined,
+ TexteVisualisierung_Text: string | undefined,
+ TextFarbe: string | undefined,
+}
+
+
+export interface ProConVisualisierungenAgg {
+ Id: number,
+ Height: number,
+ PositionX: number,
+ PositionY: number,
+ Width: number,
+ Show: boolean,
+ Discriminator: string,
+ Text: string | undefined,
+ MainView: boolean,
+ AggregateId: number,
+ Number: number | undefined,
+ PositionZ: number,
+}
+
+
+
+
+
+
+export interface ProConVisuAgg {
+ aggregat: ProConAggregate,
+ visualisierungen?: {
+ type: string,//'agg' | 'pfeil' | 'motor' | 'anfahrzeit' | 'betriebsart' | 'warnung' | 'istwert' | 'text',
+ nr?: number,
+ visuData: ProConVisualisierungenAgg,
+ xamlData?: {
+ state: string,
+ id:string,
+ xaml:string,
+ }[]
+ }[]
+}
\ No newline at end of file
diff --git a/src/types/rk/anlage.types.ts b/src/types/rk/anlage.types.ts
new file mode 100644
index 0000000..812785b
--- /dev/null
+++ b/src/types/rk/anlage.types.ts
@@ -0,0 +1,6 @@
+
+export interface Anlage{
+ id:string,
+ name:string,
+ anlageNr:number
+}
\ No newline at end of file
diff --git a/src/types/rk/elements.types.ts b/src/types/rk/elements.types.ts
new file mode 100644
index 0000000..aca7ad6
--- /dev/null
+++ b/src/types/rk/elements.types.ts
@@ -0,0 +1,66 @@
+import { GroupSubElement } from "./groups.types";
+
+export interface Element {
+ id: string,
+ anlageId: string,
+ groupId?: string,
+ name: string,
+ prefix?: string,
+ funcTxt?: string,
+ excelNr: number,
+ excelListe: "Aggregate" | "Sensoren" | "Analogwerte",
+ type: "unbekannt" | "Motor" | "Ventil" | "Analog" | "Sensor"
+
+ options?: Record
+ group: GroupSubElement;
+}
+
+export interface ElementFilterOptionen {
+ dummy?: number;
+}
+
+
+export interface ElementCreate {
+ anlageId: string,
+ groupId?: string,
+ name: string,
+ prefix?: string,
+ funcTxt?: string,
+ excelNr?: number,
+ excelListe?: "Aggregate" | "Sensoren" | "Analogwerte",
+ type: "unbekannt" | "Motor" | "Ventil" | "Analog" | "Sensor"
+
+ options?: Record
+}
+
+export interface ElementUpdate {
+ anlageId?: string,
+ groupId?: string,
+ name?: string,
+ prefix?: string,
+ funcTxt?: string,
+ excelNr?: number,
+ excelListe?: "Aggregate" | "Sensoren" | "Analogwerte",
+ type?: "unbekannt" | "Motor" | "Ventil" | "Analog" | "Sensor"
+
+ options?: Record
+}
+
+export const emptyElement: ElementCreate = {
+ anlageId: "",
+ name: "",
+ excelNr: 0,
+ excelListe: "Aggregate",
+ type: "unbekannt",
+ prefix: ""
+
+}
+
+export enum ExcelProCon {
+ Unkonwon = "keine",
+ Aggregate = "Aggregate",
+ SensorenAnalog = "SensorenAnalog",
+ SensorenDigital = "SensorenDigital",
+ AnzeigeVisu = "Anzeigen",
+ Texte = "Texte",
+}
diff --git a/src/types/rk/groups.types.ts b/src/types/rk/groups.types.ts
new file mode 100644
index 0000000..e3fc09a
--- /dev/null
+++ b/src/types/rk/groups.types.ts
@@ -0,0 +1,69 @@
+import { Element } from "./elements.types"
+
+
+export interface GroupCreate {
+ anlageId: string,
+ name: string,
+ type: string,
+ nr: string,
+ funcTxt?:string,
+ displayName?: string,
+ ort?: string,
+ info?: string,
+ //options?:Record
+ options?:Record
+}
+
+export interface GroupUpdate {
+ name?: string,
+ anlageId?: string,
+ type?: string,
+ nr?: string,
+ displayName?: string,
+ typ?: string,
+ info?: string,
+ options?:Record
+}
+
+export interface Group {
+ id: string,
+ anlageId:string;
+ name: string,
+ type: string,
+ nr: string,
+ funcTxt?:string,
+ displayName?: string,
+ ort?: string,
+ info?: string,
+ options?:Record,
+ elements:Element[],
+}
+
+export interface GroupSubElement {
+ id: string;
+ name: string;
+ type: string;
+ nr: string;
+ funcTxt?:string;
+ displayName?: string;
+ ort?: string;
+ info?: string;
+ options?:Record
+
+
+}
+
+
+export interface GroupFilterOptionen{
+ motoren?:number;
+ ventile?:number;
+
+}
+
+export const emptyGroup: GroupCreate = {
+ anlageId: '',
+ name: '',
+ type: '',
+ nr: '',
+ options:{}
+}
\ No newline at end of file
diff --git a/src/types/rk/index.ts b/src/types/rk/index.ts
new file mode 100644
index 0000000..9e4a40a
--- /dev/null
+++ b/src/types/rk/index.ts
@@ -0,0 +1,7 @@
+
+
+export * from './anlage.types';
+export * from './groups.types';
+export * from './elements.types';
+export * from './peripherie.types';
+export * from './logNotes.types';
\ No newline at end of file
diff --git a/src/types/rk/logNotes.types.ts b/src/types/rk/logNotes.types.ts
new file mode 100644
index 0000000..ce992e7
--- /dev/null
+++ b/src/types/rk/logNotes.types.ts
@@ -0,0 +1,46 @@
+export interface LogNote {
+ id: string;
+ anlageId: string;
+ groupId?: string;
+ elementId?: string;
+ peripherieId?: string;
+ topic: string;
+ content: string;
+ status: "Ok" | "Offen" | "Info"
+ createdAt: Date;
+}
+
+export interface LogNoteCreate {
+ anlageId: string;
+ groupId?: string;
+ elementId?: string;
+ peripherieId?: string;
+ topic: string;
+ content: string;
+ status: "ok" | "offen" | "info"
+ createdAt: Date;
+}
+
+export interface LogNoteUpdate {
+ anlageId?: string;
+ groupId?: string;
+ elementId?: string;
+ peripherieId?: string;
+ topic?: string;
+ content?: string;
+ status?: "ok" | "offen" | "info"
+ createdAt?: Date;
+}
+
+export interface LogNoteFilterOptionen {
+ dummy?: number;
+}
+
+export const emptyLogNote: LogNoteCreate = {
+ anlageId: "6995c44ebb286cf2c27da0d8",
+ topic: "",
+ content: "",
+ status: "info",
+ createdAt: new Date()
+
+}
\ No newline at end of file
diff --git a/src/types/rk/peripherie.types.ts b/src/types/rk/peripherie.types.ts
new file mode 100644
index 0000000..dae601e
--- /dev/null
+++ b/src/types/rk/peripherie.types.ts
@@ -0,0 +1,84 @@
+import { Element } from "./elements.types";
+import { Group } from "./groups.types";
+
+export interface ExcelImportData {
+ spsKarte:string;
+ spsAdr:string;
+ functionText:String;
+ seite:number;
+ seiteName:string;
+}
+
+export interface TcImportData {
+ FunctionText:String;
+ Adr:string;
+ kommentar:string;
+}
+
+export interface PeripherieCreate {
+ anlageId: string;
+ groupId?: string;
+ elementId?: string;
+ adrType: string;
+ adrByte: number;
+ adrBit: number;
+
+ name?: string;
+ karte?: string;
+ text?: string;
+ seite?: string;
+ seiteName?: string;
+
+ functionTxt?: string;
+ genSymbol?: string;
+
+ excelData?:ExcelImportData;
+ twinCatData?:TcImportData;
+}
+
+export interface PeripherieUpdate {
+ anlageId?: string;
+
+ adrType?: string;
+ adrByte?: number;
+ adrBit?: number;
+
+ groupId?: string;
+ elementId?: string;
+
+ name?: string;
+ karte?: string;
+ text?: string;
+ seite?: string;
+ seiteName?: string;
+ symbol?: string;
+ functionTxt?: string;
+}
+
+export interface Peripherie {
+ id: string;
+ anlageId: string;
+ adrType: string;
+ adrByte: number;
+ adrBit: number;
+ groupId?: string;
+ elementId?: string;
+ name?: string;
+ karte?: string;
+ text?: string;
+ seite?: string;
+ seiteName?: string;
+ symbol?: string;
+ functionTxt?: string;
+ element:Element;
+ group:Group;
+ excelData:ExcelImportData;
+}
+
+export interface PeripherieFilterOptionen{
+ id?:string;
+ groupId?:string;
+ elementId?:string;
+ adrByte?:number;
+
+}
diff --git a/src/types/visu/visuElements.types.ts b/src/types/visu/visuElements.types.ts
new file mode 100644
index 0000000..feca57f
--- /dev/null
+++ b/src/types/visu/visuElements.types.ts
@@ -0,0 +1,95 @@
+import { Group } from "../rk";
+import { VisuLine } from "./visuLine.types";
+
+
+
+export interface VisuElementPosition{
+ x:number;
+ y:number;
+ width:number;
+ height:number;
+
+}
+
+export interface AggVisuElemente{
+ posMotor:VisuElementPosition
+ posAnfahrwarnung:VisuElementPosition
+ posWarnung:VisuElementPosition
+ posBetriebsart:VisuElementPosition
+ posText:VisuElementPosition
+ posFuIst?:VisuElementPosition
+
+}
+
+export interface VisuElement {
+ id: string;
+ anlageId: string;
+ groupId: string;
+ elementId?: string;
+ name: string;
+ svgTypeName: string;
+ svgTypeVariant?: string;
+ svg: string;
+ height: number;
+ width: number;
+ scale?: number;
+ posX: number;
+ posY: number;
+
+ offset_x?: number,
+ offset_y?: number,
+ px_height: number,
+ px_width: number,
+ raster?: number,
+ angle?:number
+
+ group: Group;
+ lines?: VisuLine[],
+ posAggElemente?:AggVisuElemente
+}
+
+export interface VisuElementCreate {
+ anlageId: string;
+ name: string;
+ groupId: string;
+ elementId?: string;
+ svgTypeName: string;
+ svgTypeVariant?: string;
+ scale?: number;
+ posX: number;
+ posY: number;
+ lines?: VisuLine[],
+ posAggElemente?:AggVisuElemente
+}
+
+export interface VisuElementUpdate {
+ anlageId?: string;
+ name?: string;
+ groupId?: string;
+ elementId?: string;
+ svgTypeName?: string;
+ svgTypeVariant?: string;
+ scale?: number;
+ posX?: number;
+ posY?: number;
+ hight?: number;
+ width?: number;
+ lines?: VisuLine[],
+ posAggElemente?:AggVisuElemente
+
+}
+
+export const emptyVisuElement = {
+ _id: "",
+ anlageId: "",
+ groupId: "",
+ name: "",
+ svgTypeName: "",
+ svg: "",
+ height: 0,
+ width: 0,
+ posX: 0,
+ posY: 0
+
+
+}
\ No newline at end of file
diff --git a/src/types/visu/visuLine.types.ts b/src/types/visu/visuLine.types.ts
new file mode 100644
index 0000000..c34ede7
--- /dev/null
+++ b/src/types/visu/visuLine.types.ts
@@ -0,0 +1,16 @@
+
+export interface VisuLinePoint {
+ x: number,
+ y: number
+}
+
+export interface VisuLine {
+ bezeichnung: string;
+ offsetX: number;
+ offsetY: number;
+ points: VisuLinePoint[];
+ color: string;
+ px_width?: number;
+ px_height?: number;
+ svg?:string
+}
diff --git a/tsconfig.json b/tsconfig.json
index cf9c65d..2d2d31a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -28,7 +28,6 @@
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
- "**/*.mts"
- ],
+ "**/*.mts" ],
"exclude": ["node_modules"]
}