diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4779935..3c5c121 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,18 @@ "name": "rk-frontend", "version": "0.1.0", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@radix-ui/react-slot": "^1.3.3", + "@xmldom/xmldom": "^0.9.11", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.34.0", "next": "16.1.6", "react": "19.2.3", - "react-dom": "19.2.3" + "react-dom": "19.2.3", + "react-hook-form": "^7.71.2", + "tailwind-merge": "^3.6.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -33,6 +42,55 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", @@ -653,6 +711,37 @@ "node": ">= 10" } }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -930,7 +1019,7 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, + "devOptional": true, "dependencies": { "csstype": "^3.2.2" } @@ -944,6 +1033,14 @@ "@types/react": "^19.2.0" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.11.tgz", + "integrity": "sha512-tW8bcK3hsG0/uqSnNz6TK4BkcuZSezoU7DlnYssILmZDktPnSHHuDJJFM0AJv+13gz2r0iGdrj6qqKeUnxXEDg==", + "engines": { + "node": ">=14.6" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", @@ -974,16 +1071,35 @@ } ] }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true + "devOptional": true }, "node_modules/detect-libc": { "version": "2.1.2", @@ -1271,6 +1387,14 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lucide-react": { + "version": "1.34.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.34.0.tgz", + "integrity": "sha512-vnjGJNI7Htk5+oWW8gXGuaLgwgAb0T6/iZbBrp9JCfRFwdNWZ0YTm3eyxjOLgwN6r8iyAf3UA70zNmBRBNv7yg==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1428,6 +1552,21 @@ "react": "^19.2.3" } }, + "node_modules/react-hook-form": { + "version": "7.71.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", + "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -1519,6 +1658,15 @@ } } }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.0.tgz", diff --git a/package.json b/package.json index 706724a..ceb377d 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,21 @@ "scripts": { "dev": "next dev", "build": "next build", - "start": "next start" + "start": "next start -p 4010" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@radix-ui/react-slot": "^1.3.3", + "@xmldom/xmldom": "^0.9.11", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.34.0", "next": "16.1.6", "react": "19.2.3", - "react-dom": "19.2.3" + "react-dom": "19.2.3", + "react-hook-form": "^7.71.2", + "tailwind-merge": "^3.6.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/src/_config/pages.dio b/src/_config/pages.dio new file mode 100644 index 0000000..f588f30 --- /dev/null +++ b/src/_config/pages.dio @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/_config/sites.ts b/src/_config/sites.ts new file mode 100644 index 0000000..ff38303 --- /dev/null +++ b/src/_config/sites.ts @@ -0,0 +1,73 @@ +import { difference } from "next/dist/build/utils" + +const rkPrefix = '/rk' + +export const Sites = { + main: { url: '/', name: 'Home' }, + anlagen: { + url: `${rkPrefix}/anlagen`, + name: 'Anlagen', + new: { url: `${rkPrefix}/anlagen/new` }, + update: { url: `${rkPrefix}/anlagen/update`, param: 'id' } + }, + groups: { + url: `${rkPrefix}/groups`, + name: 'Gruppen', + new: { url: `${rkPrefix}/groups/new`, name:'Neue Gruppe' }, + update: { url: `${rkPrefix}/groups/update`, param: 'id' } + }, + elements: { + url: `${rkPrefix}/elements`, + name: 'Elemente', + new: { url: `${rkPrefix}/elements/new`, name:'Neues Element Anlegen' }, + update: { url: `${rkPrefix}/elements/update`, param: 'id' } + }, + peripheries: { + url: `${rkPrefix}/peripheries`, + name: 'Ein/Ausgänge', + new: { url: `${rkPrefix}/peripheries/new` }, + update: { url: `${rkPrefix}/peripheries/update`, param: 'id' }, + differences: { url: `${rkPrefix}/peripheries/differences`, name: "Unterschiede EA Liste -Excel" } + }, + logNote: { + url: `${rkPrefix}/log-note`, + name: 'Notizen', + new: { url: `${rkPrefix}/log-note/new`,name:"Neue Notiz" }, + update: { url: `${rkPrefix}/log-note/update`, name:"Notiz Bearbeiten", param: 'id' }, + }, + visu: { + url: `visu`, + name: 'Visualisierung', + create: { url: `visu/create`, name:"Create Element" }, + elements: { url: `visu/elements`, name:"Visu Elemente" }, + selectGroups: { url: `visu/groups_add`, name:"Gruppen zu Visu hinzufügen" }, + }, + procon: { + url: `/procon`, + name: 'DB Procon', + aggregate: { url: `/procon/aggregate`, name:"Übersicht Aggregate" }, + texteVisu: { url: `/procon/visu_texte`, name:"Visualisierung Texte" }, + sensoren: { url: `/procon/sensoren`, name:"Visualisierung Texte" }, + anzeigen: { url: `/procon/anzeigen`, name:"Visualisierung Anzeigen" }, + svgTest: { url: `/procon/svg`, name:"SVG test" }, + }, + rk2: { + url: `/rk2`, + name: 'Anlagen (neu-RK2)', + groups: { + url: `/rk2/groups`, name:"Gruppen RK2" , + new:{url: `/rk2/groups/edit/new`, name:"Neue Gruppe" }, + edit:{url: `/rk2/groups/edit`, name:"Gruppe Ändern" } + }, + units: { + url: `/rk2/units`, + name:"Units", + new:{url: `/rk2/units//edit/new`, name:"Neue Unit" } + }, + signals: { url: `/rk2/signale`, name:"Signale" }, + ios: { url: `/rk2/ios`, name:"IO " }, + } + + + +} \ No newline at end of file diff --git a/src/_context/visu/VisuElements.types.ts b/src/_context/visu/VisuElements.types.ts new file mode 100644 index 0000000..324d4e2 --- /dev/null +++ b/src/_context/visu/VisuElements.types.ts @@ -0,0 +1,48 @@ +import { VisuElement } from "@/types/visu/visuElements.types"; + +export type DragMode = "idle" | "canvas" | "element"; + +export interface HistoryState { + past: T[]; + present: T; + future: T[]; +} + +export interface DragState { + mode: DragMode; + elementId?: string; + startX: number; + startY: number; + lastX: number; + lastY: number; +} +export type VisuElementsState = { + drag: DragState; + elements: VisuElement[]; + selectedElement: VisuElement | undefined; + editSession: { + active: boolean; + source: "drag" | "panel" | null; + }; + test: string; +}; + +export type VisuHistoryState = HistoryState; + + +export type VisuElementsActions = + | { type: "SET_ELEMENTS"; payload: VisuElement[] } + | { type: "SELECT_ELEMENT"; payload: VisuElement } + | { type: "DESELECT_ELEMENT" } + | { type: "DRAG_START"; payload: { mode: "canvas" | "element"; elementId?: string; x: number; y: number } } + | { type: "DRAG_MOVE"; payload: { x: number; y: number } } + | { type: "DRAG_END" } + | { type: "MOVE_SELECTED_ELEMENT"; payload: { dx: number, dy: number } } + | { type: "UPDATE_ELEMENT"; payload: VisuElement } + | { type: "UNDO" } + | { type: "REDO" } + | { type: "SET_STATE"; payload: VisuElementsState } + | { type: "UPDATE_SCALE"; payload: number } + | { type: "UPDATE_LINE_OFFSET"; payload: Partial<{x:number,y:number}> } + + ; \ No newline at end of file diff --git a/src/_context/visu/VisuElementsContext.ts b/src/_context/visu/VisuElementsContext.ts new file mode 100644 index 0000000..e8155a0 --- /dev/null +++ b/src/_context/visu/VisuElementsContext.ts @@ -0,0 +1,9 @@ +import { createContext, Dispatch } from "react"; +import { VisuElementsActions, VisuElementsState } from "./VisuElements.types"; + +type VisuElementsContextType = { + state: VisuElementsState; + dispatch: Dispatch; +}; + +export const VisuElementsContext = createContext(undefined); \ No newline at end of file diff --git a/src/_context/visu/VisuElementsProvider.tsx b/src/_context/visu/VisuElementsProvider.tsx new file mode 100644 index 0000000..3f22d47 --- /dev/null +++ b/src/_context/visu/VisuElementsProvider.tsx @@ -0,0 +1,37 @@ +"use client"; +import { useEffect, useReducer } from "react"; +import { VisuElementsState } from "./VisuElements.types"; +import { VisuElementsReducer } from "./VisuElementsReducer"; +import { VisuElementsContext } from "./VisuElementsContext"; +import { VisuElement } from "@/types/visu/visuElements.types"; + + + + +const initialState: VisuElementsState = { + drag: { + mode: "idle", + startX: 0, + startY: 0, + lastX: 0, + lastY: 0, + }, + elements: [], + selectedElement: undefined, + editSession: {active:false,source:null}, + test: "", +}; + + +export function VisuElementsProvider({ children, initialValues }: { children: React.ReactNode, initialValues: VisuElement[] }) { + const [state, dispatch] = useReducer(VisuElementsReducer, initialState); + + useEffect(() => { + dispatch({ type: "SET_ELEMENTS", payload: initialValues }) + }, [initialValues]) + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/src/_context/visu/VisuElementsReducer.ts b/src/_context/visu/VisuElementsReducer.ts new file mode 100644 index 0000000..6850cb7 --- /dev/null +++ b/src/_context/visu/VisuElementsReducer.ts @@ -0,0 +1,180 @@ +import { stat } from "fs"; +import { VisuElementsActions, VisuElementsState } from "./VisuElements.types"; + +const helper = (state:VisuElementsState):VisuElementsState=>{ + return state; +} +export function VisuElementsReducer(state: VisuElementsState, action: VisuElementsActions): VisuElementsState { + switch (action.type) { + + // ----------------------- + // SET ELEMENTS + // ----------------------- + case "SET_ELEMENTS": + return { + ...state, + elements: action.payload, + test: "daten wurden gleaden" + } + + // ----------------------- + // SELECT + // ----------------------- + case "SELECT_ELEMENT": + return { + ...state, + selectedElement: action.payload, + }; + + case "DESELECT_ELEMENT": + return { + ...state, + selectedElement: undefined, + }; + + + // ----------------------- + // DRAG START + // ----------------------- + case "DRAG_START": + + return { + ...state, + drag: { + mode: action.payload.mode, + elementId: action.payload.elementId, + startX: action.payload.x, + startY: action.payload.y, + lastX: action.payload.x, + lastY: action.payload.y, + }, + }; + + // ----------------------- + // DRAG MOVE + // ----------------------- + case "DRAG_MOVE": { + const drag = state.drag; + + const dx = action.payload.x - drag.lastX; + const dy = action.payload.y - drag.lastY; + if (state.drag.mode !== "element") return state; + // ----------------------- + // ELEMENT DRAG + // ----------------------- + if (drag.mode === "element" && drag.elementId) { + let updatedElement: any = null; + + const elements = state.elements.map((el) => { + if (el.id !== drag.elementId) return el; + + const newX = el.posX + dx; + const newY = el.posY + dy; + + updatedElement = { + ...el, + posX: newX, + posY: newY, + }; + + return updatedElement; + }); + + return { + ...state, + elements, + drag: { + ...drag, + lastX: action.payload.x, + lastY: action.payload.y, + }, + selectedElement: + state.selectedElement?.id === drag.elementId + ? updatedElement + : state.selectedElement, + }; + } + + // ----------------------- + // CANVAS DRAG + // ----------------------- + if (drag.mode === "canvas") { + return { + ...state, + drag: { + ...drag, + lastX: action.payload.x, + lastY: action.payload.y, + }, + // ❗ KEIN state change hier + }; + } + + return state; + } + + // ----------------------- + // DRAG END (snap) + // ----------------------- + case "DRAG_END": + return { + ...state, + drag: { + mode: "idle", + elementId: undefined, + startX: 0, + startY: 0, + lastX: 0, + lastY: 0, + }, + }; + + + // ----------------------- + // KEYBOARD MOVE + // ----------------------- + case "MOVE_SELECTED_ELEMENT": + if (!state.selectedElement) return state; + + const moved = { + ...state.selectedElement, + posX: state.selectedElement.posX + action.payload.dx, + posY: state.selectedElement.posY + action.payload.dy, + }; + + return { + ...state, + selectedElement: moved, + elements: state.elements.map((el) => + el.id === moved.id ? moved : el + ), + }; + + // ----------------------- + // UPDATE + // ----------------------- + case "UPDATE_ELEMENT": + const updated =state.elements.map(el => + el.id === action.payload.id ? action.payload : el + ); + + return { + ...state, + elements: updated, + selectedElement: + state.selectedElement?.id === action.payload.id + ? action.payload + : state.selectedElement, + }; + // ----------------------- + // UPDATE + // ----------------------- + case "UPDATE_LINE_OFFSET": + + return state; + + default: + return state; + } +} + diff --git a/src/_context/visu/useVisuElementsContext.ts b/src/_context/visu/useVisuElementsContext.ts new file mode 100644 index 0000000..9d7d758 --- /dev/null +++ b/src/_context/visu/useVisuElementsContext.ts @@ -0,0 +1,13 @@ +import { useContext } from "react"; +import { VisuElementsContext } from "./VisuElementsContext"; + + +export function useVisuElementContext() { + const context = useContext(VisuElementsContext); + + if (!context) { + throw new Error("useFormContext must be used within FormProvider"); + } + + return context; +} \ No newline at end of file diff --git a/src/actions/draw/api-draw.ts b/src/actions/draw/api-draw.ts new file mode 100644 index 0000000..0baedab --- /dev/null +++ b/src/actions/draw/api-draw.ts @@ -0,0 +1,22 @@ +"use server" + + + +const url = "http://localhost:3000/draw"; + + +//----------------------------------------------------------------------------- +//--- Get Data of Grafik +//----------------------------------------------------------------------------- +export async function api_drawData(name?:string): Promise { + name =name ? name: "anlageDurmin"; + + const result = await fetch(`${url}/${name}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return (data); +} diff --git a/src/actions/procon/api-procon-aggregate.ts b/src/actions/procon/api-procon-aggregate.ts new file mode 100644 index 0000000..7c8178c --- /dev/null +++ b/src/actions/procon/api-procon-aggregate.ts @@ -0,0 +1,38 @@ +"use server" + +import { revalidatePath } from "next/cache"; +import { Sites } from "@/_config/sites"; +import { ProConAggregate, ProConAggVisu } from "@/types/procon"; + + +const url = "http://localhost:3000/procon/"; +const url_aggrgate = url + "aggregate" + +export async function api_ProconAggregate_GetAll(): Promise { + + const result = await fetch(`${url_aggrgate}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return orderById(data); +} + +export async function api_ProconAggregate_GetById(id:number): Promise { + + const result = await fetch(`${url_aggrgate}/byAntriebsId/${id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + + +function orderById(data:ProConAggregate[]) { + return data.sort((a,b)=>a.SPSAntriebId-b.SPSAntriebId) +} \ No newline at end of file diff --git a/src/actions/procon/api-procon-anzeigen.ts b/src/actions/procon/api-procon-anzeigen.ts new file mode 100644 index 0000000..4e7d8d8 --- /dev/null +++ b/src/actions/procon/api-procon-anzeigen.ts @@ -0,0 +1,64 @@ +"use server" + +import { ProConAnzeige, ProConAnzeigeVisuElement } from "@/types/procon/anzeigen"; + + +const url = "http://localhost:3000/procon/"; +const url_anzeigen = url + "anzeigen" + +//----------------------------------------------------------------------------- +//--- Anzeigen +//----------------------------------------------------------------------------- +export async function api_ProconAnzeigen(): Promise { + + const result = await fetch(`${url_anzeigen}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return orderByDatenNummer(data); +} + +export async function api_ProconAnzeige_byId(id:number): Promise { + + const result = await fetch(`${url_anzeigen}/byId/${id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +export async function api_ProconAnzeige_byDatenNumber(id:number): Promise { + + const result = await fetch(`${url_anzeigen}/byDatenNummer/${id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +//--- Visu Elemente +export async function api_ProconAnzeige_visuElements(id:number): Promise { + + const result = await fetch(`${url_anzeigen}/VisuElements/${id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + + +function orderByDatenNummer(data:ProConAnzeige[]) { + return data.sort((a,b)=>a.DatenNummer-b.DatenNummer) +} \ No newline at end of file diff --git a/src/actions/procon/api-procon-meldungen.ts b/src/actions/procon/api-procon-meldungen.ts new file mode 100644 index 0000000..25f9267 --- /dev/null +++ b/src/actions/procon/api-procon-meldungen.ts @@ -0,0 +1,50 @@ +"use server" + +import { ProConMeldung } from "@/types/procon/meldungen"; + + + +const url = "http://localhost:3000/procon/"; +const url_meldungen = url + "meldungen" + +//----------------------------------------------------------------------------- +//--- Meldungen +//----------------------------------------------------------------------------- +export async function api_ProconMeldungen(): Promise { + + const result = await fetch(`${url_meldungen}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +export async function api_ProconMeldung_byId(id:number): Promise { + + const result = await fetch(`${url_meldungen}/byId/${id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +export async function api_ProconMeldung_byAggregateId(id:number): Promise { + + const result = await fetch(`${url_meldungen}/byAggregateId/${id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + + + diff --git a/src/actions/procon/api-procon-sensoren.ts b/src/actions/procon/api-procon-sensoren.ts new file mode 100644 index 0000000..2814632 --- /dev/null +++ b/src/actions/procon/api-procon-sensoren.ts @@ -0,0 +1,117 @@ +"use server" + +import { ProConSensor, ProConSensorVisuElement } from "@/types/procon/sensoren"; + + +const url = "http://localhost:3000/procon/"; +const url_sensoren = url + "sensoren" +const digital ="digital"; +const analog = "analog"; + +//----------------------------------------------------------------------------- +//--- Digital Sensoren +//----------------------------------------------------------------------------- +export async function api_ProconSensoren_digital(): Promise { + + const result = await fetch(`${url_sensoren}/${digital}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return orderByDatenNummer(data); +} + +export async function api_ProconSensor_digital_byId(id:number): Promise { + + const result = await fetch(`${url_sensoren}/${digital}/byId/${id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +export async function api_ProconSensor_digital_byTextNumber(id:number): Promise { + + const result = await fetch(`${url_sensoren}/${digital}/byDatenNummer/${id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +//--- Visu Elemente +export async function api_ProconSensor_digital_visuElements(id:number): Promise { + + const result = await fetch(`${url_sensoren}/${digital}/VisuElements/${id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +//----------------------------------------------------------------------------- +//--- Analog Sensoren +//----------------------------------------------------------------------------- +export async function api_ProconSensoren_analog(): Promise { + + const result = await fetch(`${url_sensoren}/${analog}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return orderByDatenNummer(data); +} + +export async function api_ProconSensor_analog_byId(id:number): Promise { + + const result = await fetch(`${url_sensoren}/${analog}/byId/${id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +export async function api_ProconSensor_analog_byTextNumber(id:number): Promise { + + const result = await fetch(`${url_sensoren}/${analog}/byDatenNummer/${id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +//--- Visu Elemente +export async function api_ProconSensor_analog_visuElements(id:number): Promise { + + const result = await fetch(`${url_sensoren}/${analog}/VisuElements/${id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +function orderByDatenNummer(data:ProConSensor[]) { + return data.sort((a,b)=>a.DatenNummer-b.DatenNummer) +} \ No newline at end of file diff --git a/src/actions/procon/api-procon-texte.ts b/src/actions/procon/api-procon-texte.ts new file mode 100644 index 0000000..28be9a0 --- /dev/null +++ b/src/actions/procon/api-procon-texte.ts @@ -0,0 +1,48 @@ +"use server" + +import { ProConText } from "@/types/procon/texte"; + + +const url = "http://localhost:3000/procon/"; +const url_texte = url + "texte" + +export async function api_ProconTexte_getTexte(): Promise { + + const result = await fetch(`${url_texte}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return orderByNummer(data); +} + +export async function api_ProconTexte_byId(id:number): Promise { + + const result = await fetch(`${url_texte}/byId/${id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +export async function api_ProconTexte_byTextNumber(id:number): Promise { + + const result = await fetch(`${url_texte}/byTextNumber/${id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + + +function orderByNummer(data:ProConText[]) { + return data.sort((a,b)=>a.Nummer-b.Nummer) +} \ No newline at end of file diff --git a/src/actions/procon/api-procon-views.ts b/src/actions/procon/api-procon-views.ts new file mode 100644 index 0000000..bad3a3b --- /dev/null +++ b/src/actions/procon/api-procon-views.ts @@ -0,0 +1,34 @@ +"use server" + +import { revalidatePath } from "next/cache"; +import { Sites } from "@/_config/sites"; + + +const url = "http://localhost:3000/procon/"; +const url_views = url + "views" + +export async function api_ProconViews_UpdateXAML(id: string, xamlBase64: string): Promise { + + let updateData = { + "xamlBase64": xamlBase64 + } + console.log(updateData,id) + const result = await fetch(`${url_views}/xaml/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updateData), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.procon.url); + return { success: true }; + +} \ No newline at end of file diff --git a/src/actions/procon/api-procon-visualisierungen.ts b/src/actions/procon/api-procon-visualisierungen.ts new file mode 100644 index 0000000..fd62f3d --- /dev/null +++ b/src/actions/procon/api-procon-visualisierungen.ts @@ -0,0 +1,39 @@ +"use server" + +import { revalidatePath } from "next/cache"; +import { Sites } from "@/_config/sites"; +import { ProConAggregate, ProConView } from "@/types/procon"; + + +const url = "http://localhost:3000/procon/"; +const url_visualisierungen = url + "visu" + +export async function api_ProconVisualisierungen_Update(id: number, data: Partial): Promise { + + let updateData = { + "PositionX": data.posX, + "PositionY": data.posY, + "Height": data.height, + "Width": data.width, + "PositionZ":data.zIndex, + } + console.log(updateData,id) + const result = await fetch(`${url_visualisierungen}/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updateData), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.procon.url); + return { success: true }; + +} \ No newline at end of file diff --git a/src/actions/rk/api-anlage.ts b/src/actions/rk/api-anlage.ts new file mode 100644 index 0000000..78c1cd5 --- /dev/null +++ b/src/actions/rk/api-anlage.ts @@ -0,0 +1,53 @@ +'use server' + +import { Sites } from "@/_config/sites"; +import { Anlage } from "@/types/rk"; +import { revalidatePath } from "next/cache"; + +export async function api_anlagen_getAll(): Promise { + + const result = await fetch(`http://localhost:3000/anlage`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +export async function api_anlage_getSelected(): Promise { + + const result = await fetch(`http://localhost:3000/anlage/selected`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +export async function api_anlage_setSelected(id: string): Promise { + console.log(`http://localhost:3000/anlage/selected/${id}`); + + const result = await fetch(`http://localhost:3000/anlage/selected/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + + console.log(result.status, result.statusText); + + if (!result.ok) { + const error = await result.text(); + throw new Error(`API Fehler ${result.status}: ${error}`); + } + + revalidatePath(Sites.anlagen.url); + return data; + +} + diff --git a/src/actions/rk/api-draw.ts b/src/actions/rk/api-draw.ts new file mode 100644 index 0000000..794f3d2 --- /dev/null +++ b/src/actions/rk/api-draw.ts @@ -0,0 +1,28 @@ +import { DrawShapeData, DrawShapeOptionen } from "@/types/draw/draw.types"; +import { Anlage } from "@/types/rk"; + +export async function drawGetSvg(name: string, optionen?: DrawShapeOptionen): Promise { + + let optionenStr = ''; + if (optionen) { + optionenStr = '?' + + let separator = '' + for (const [key, value] of Object.entries(optionen)) { + + optionenStr += separator + key + "=" + value; + separator = '&' + } + console.log(optionenStr) + + } + + const result = await fetch(`http://localhost:3000/draw/gurtfoerderer${optionenStr}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json() as DrawShapeData; + + return data; +} \ No newline at end of file diff --git a/src/actions/rk/api-element.ts b/src/actions/rk/api-element.ts new file mode 100644 index 0000000..4c1376d --- /dev/null +++ b/src/actions/rk/api-element.ts @@ -0,0 +1,96 @@ +"use server" + +import { Sites } from "@/_config/sites"; +import { Element, ElementCreate, ElementFilterOptionen, ElementUpdate } from "@/types/rk"; +import { revalidatePath } from "next/cache"; + +export async function api_ElementsGet(filter?: ElementFilterOptionen): Promise { + + let filterStr = ''; + + if (filter) { + filterStr = '?' + + let separator = '' + for (const [key, value] of Object.entries(filter)) { + + filterStr += separator + key + "=" + value; + separator = '&' + } + } + + + const result = await fetch(`http://localhost:3000/element${filterStr}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +export async function elementCreate(data: ElementCreate): Promise> { + + const result = await fetch('http://localhost:3000/element', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.elements.url+"/") + return { success: true }; +} + +export async function elementUpdate(id: string, data: ElementUpdate): Promise> { + + const result = await fetch(`http://localhost:3000/element/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.elements.url); + return { success: true }; + +} + + +export async function api_elementDelete(id: string): Promise> { + + const result = await fetch(`http://localhost:3000/element/${id}`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' } + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.elements.url); + return { success: true }; + +} \ No newline at end of file diff --git a/src/actions/rk/api-file-uplaod.ts b/src/actions/rk/api-file-uplaod.ts new file mode 100644 index 0000000..75d38a1 --- /dev/null +++ b/src/actions/rk/api-file-uplaod.ts @@ -0,0 +1,24 @@ +'use server'; + +export async function uploadFile(formData: FormData) { + const file = formData.get('file') as File; + + if (!file) { + throw new Error('Keine Datei'); + } + + // Neues FormData für NestJS + const backendForm = new FormData(); + backendForm.append('file', file); + + const res = await fetch('http://localhost:3000/draw/upload', { + method: 'POST', + body: backendForm, + }); + + if (!res.ok) { + throw new Error('Upload fehlgeschlagen'); + } + + return await res.json(); +} \ No newline at end of file diff --git a/src/actions/rk/api-groups.ts b/src/actions/rk/api-groups.ts new file mode 100644 index 0000000..ae00618 --- /dev/null +++ b/src/actions/rk/api-groups.ts @@ -0,0 +1,97 @@ +"use server" + +import { Sites } from "@/_config/sites"; +import { Group, GroupCreate, GroupFilterOptionen, GroupUpdate } from "@/types/rk"; +import { revalidatePath } from "next/cache"; + +export async function api_GroupAll(filter?: GroupFilterOptionen): Promise { + + let filterStr = ''; + + if (filter) { + filterStr = '?' + + let separator = '' + for (const [key, value] of Object.entries(filter)) { + + filterStr += separator + key + "=" + value; + separator = '&' + } + console.log(filterStr) + console.log(`http://localhost:3000/group${filter}`); + } + + + const result = await fetch(`http://localhost:3000/group${filterStr}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +export async function groupCreate(data: GroupCreate): Promise> { + + const result = await fetch('http://localhost:3000/group', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.groups.url) + return { success: true,data:resultData }; +} + +export async function groupUpdate(id: string, data: GroupUpdate): Promise> { + + const result = await fetch(`http://localhost:3000/group/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.groups.url); + return { success: true,data:resultData }; + +} + +export async function api_groupDelete(id: string): Promise> { + console.log('delete Group') + const result = await fetch(`http://localhost:3000/group/${id}`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' } + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.groups.url); + return { success: true,data:resultData }; + +} \ No newline at end of file diff --git a/src/actions/rk/api-logNote.ts b/src/actions/rk/api-logNote.ts new file mode 100644 index 0000000..edab21b --- /dev/null +++ b/src/actions/rk/api-logNote.ts @@ -0,0 +1,97 @@ +"use server" + +import { Sites } from "@/_config/sites"; +import { LogNote, LogNoteCreate, LogNoteFilterOptionen, LogNoteUpdate } from "@/types/rk"; +import { revalidatePath } from "next/cache"; + +const path ="log-note" +export async function api_LogNoteGet(filter?: LogNoteFilterOptionen): Promise { + + let filterStr = ''; + + if (filter) { + filterStr = '?' + + let separator = '' + for (const [key, value] of Object.entries(filter)) { + + filterStr += separator + key + "=" + value; + separator = '&' + } + } + + + const result = await fetch(`http://localhost:3000/${path}${filterStr}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +export async function api_logNoteCreate(data: LogNoteCreate): Promise> { + + const result = await fetch(`http://localhost:3000/${path}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + // revalidatePath(Sites.elements.url) + return { success: true }; +} + +export async function api_logNoteUpdate(id: string, data: LogNoteUpdate): Promise> { + + const result = await fetch(`http://localhost:3000/${path}/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.logNote.url); + return { success: true }; + +} + + +export async function api_logNoteDelete(id: string): Promise> { + + const result = await fetch(`http://localhost:3000/${path}/${id}`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' } + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.logNote.url); + return { success: true }; + +} \ No newline at end of file diff --git a/src/actions/rk/api-peripherie.ts b/src/actions/rk/api-peripherie.ts new file mode 100644 index 0000000..ab9d7e9 --- /dev/null +++ b/src/actions/rk/api-peripherie.ts @@ -0,0 +1,60 @@ +"use server"; + +import { Sites } from "@/_config/sites"; +import { Group, Peripherie, PeripherieFilterOptionen, PeripherieUpdate } from "@/types/rk"; +import { revalidatePath } from "next/cache"; + + + +export async function PeripherieFindApi(filter?: PeripherieFilterOptionen): Promise { + let filterStr = ''; + if (filter && (filter.adrByte || filter.elementId || filter.groupId)) { + filterStr = '?' + if (filter.adrByte) filterStr += 'adrByte=' + filter.adrByte; + if (filter.groupId) filterStr += 'groupId=' + filter.groupId; + if (filter.elementId) filterStr += 'elementId=' + filter.elementId; + } + console.log(`http://localhost:3000/peripherie` + filterStr) + const result = await fetch(`http://localhost:3000/peripherie` + filterStr, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +export async function GetEA_Differences(): Promise { + + const result = await fetch(`http://localhost:3000/peripherie/differences`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +export async function api_peripherieUpdate(id: string, data: PeripherieUpdate): Promise> { + + const result = await fetch(`http://localhost:3000/peripherie/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.peripheries.url); + return { success: true }; + +} \ No newline at end of file diff --git a/src/actions/rk/api-visuElements.ts b/src/actions/rk/api-visuElements.ts new file mode 100644 index 0000000..201a439 --- /dev/null +++ b/src/actions/rk/api-visuElements.ts @@ -0,0 +1,64 @@ +'use server' + +import { Sites } from "@/_config/sites"; +import { VisuElement, VisuElementCreate, VisuElementUpdate } from "@/types/visu/visuElements.types"; +import { revalidatePath } from "next/cache"; + +export async function api_VisuElementsAll(): Promise { + + + + const result = await fetch(`http://localhost:3000/visu-elements`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json() as VisuElement[]; + + return data; +} +export async function api_VisuElementCreate(data: VisuElementCreate): Promise> { + + + const result = await fetch(`http://localhost:3000/visu-elements/`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.visu.url); + return { success: true }; + +} + +export async function api_VisuElementUpdate(id: string, data: VisuElementUpdate): Promise> { + + + const result = await fetch(`http://localhost:3000/visu-elements/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.visu.url); + return { success: true }; + +} diff --git a/src/actions/rk2/api-compTypes.ts b/src/actions/rk2/api-compTypes.ts new file mode 100644 index 0000000..a51a056 --- /dev/null +++ b/src/actions/rk2/api-compTypes.ts @@ -0,0 +1,481 @@ +import { ComponentDefaults, ComponentTypeDefinition } from "@/types/anlagen/components"; +import { SignalType } from "@/types/anlagen/signals"; + +export const componentTypeDefinitions: ComponentTypeDefinition[] = [ + { + type: "Antrieb", + label: "Antrieb", + + properties: [ + { + key: "linkslauf", + label: "Linkslauf", + type: "boolean", + defaultValue: false, + required: false, + }, + { + key: "direkt", + label: "Direkt", + type: "boolean", + defaultValue: true, + required: false, + }, + { + key: "sternDreieck", + label: "Ster-Dreieck", + type: "boolean", + defaultValue: false, + required: false, + }, + { + key: "fu", + label: "Frequenzumrichter", + type: "boolean", + defaultValue: false, + required: false, + }, + { + key: "rm", + label: "Rueckmeldung", + type: "boolean", + defaultValue: true, + required: false, + }, + { + key: "ms", + label: "Motorschutz", + type: "boolean", + defaultValue: true, + required: false, + }, + { + key: "ptc", + label: "PTC", + type: "boolean", + defaultValue: false, + required: false, + }, + { + key: "ts", + label: "Thermoschutz", + type: "boolean", + defaultValue: false, + required: false, + }, + ], + }, + + { + type: "Frequenzumrichter", + label: "Frequenzumrichter", + + properties: [ + { + key: "manufacturer", + label: "Hersteller", + type: "text", + defaultValue: 'Schneider', + }, + { + key: "power", + label: "Leistung [kW]", + type: "number", + defaultValue: "5", + }, + { + key: "maxFrequency", + label: "Max. Frequenz [Hz]", + type: "number", + defaultValue: 50, + }, + ], + }, + + { + type: "Reparaturschalter", + label: "Reparaturschalter", + + properties: [ + + ], + }, + + { + type: "Endschalter", + label: "Endschalter", + + properties: [ + { + key: "normallyClosed", + label: "Öffner", + type: "boolean", + defaultValue: false, + }, + ], + }, + + { + type: "Schieflaufwächter", + label: "Schieflaufwächter", + + properties: [ + { + key: "warnung", + label: "Warnung", + type: "boolean", + defaultValue: false, + }, + ], + }, + + { + type: "Reißleine", + label: "Reißleine", + + properties: [ + { + key: "safty", + label: "Safety", + type: "boolean", + defaultValue: true, + }, + ], + }, + + { + type: "Drehzahl", + label: "Drehzahl", + + properties: [ + { + key: "impuls", + label: "Impulssignal", + type: "boolean", + defaultValue: true, + }, + { + key: "dauer", + label: "Dauersignal", + type: "boolean", + defaultValue: false, + }, + ], + }, + + { + type: "Vorortbedienung", + label: "Vorortbedienung", + + properties: [ + { + key: "ta_ein", + label: "Ein Taster", + type: "boolean", + defaultValue: true, + }, + { + key: "ta_ein_rtg2", + label: "Taster Ein Richtung 2", + type: "boolean", + defaultValue: false, + }, + { + key: "ta_aus_no", + label: "Taster Aus (no)", + type: "boolean", + defaultValue: false, + }, + { + key: "ta_aus_nc", + label: "Taster Aus (nc)", + type: "boolean", + defaultValue: false, + } + ], + }, + { + type: "Fuellstand", + label: "Füllstand", + + properties: [ + + ], + }, + { + type: "Überwachung", + label: "Überwachung", + + properties: [ + { + key: "monitoringType", + label: "Überwachungstyp", + type: "select", + + options: [ + { + value: "strom", + label: "Strom", + }, + { + value: "temperature", + label: "Temperatur", + }, + { + value: "pressure", + label: "Druck", + }, + { + value: "vibration", + label: "Vibration", + }, + ], + defaultValue: "strom", + + }, + ], + }, +] + +export const componentDefaults: ComponentDefaults[] = [ + { + type: "Antrieb", + name: "M", + + signals: [ + { name: "start", type: SignalType.DIGITAL_OUTPUT }, + { name: "startRtg2", type: SignalType.DIGITAL_OUTPUT }, + { name: "rm", type: SignalType.DIGITAL_INPUT }, + { name: "msOk", type: SignalType.DIGITAL_INPUT }, + { name: "tsOk", type: SignalType.DIGITAL_INPUT }, + { name: "ptcOk", type: SignalType.DIGITAL_INPUT }, + ], + }, + + { + type: "Frequenzumrichter", + name: "Fu", + signals: [], + }, + + { + type: "Reparaturschalter", + name: "Hs", + signals: [ + { name: "HsEin", type: SignalType.DIGITAL_INPUT }, + ], + + }, + { + type: "Endschalter", + name: "Es", + signals: [ + { name: "Pos", type: SignalType.DIGITAL_INPUT }, + ], + + }, + + { + type: "Schieflaufwächter", + name: "Sl", + signals: [ + { name: "SlOk", type: SignalType.DIGITAL_INPUT }, + { name: "Sl_Warnung", type: SignalType.DIGITAL_INPUT }, + ], + }, + + { + type: "Reißleine", + name: "Rl", + signals: [ + { name: "RlOk", type: SignalType.DIGITAL_INPUT }, + { name: "RlQuitt", type: SignalType.DIGITAL_OUTPUT }, + ], + }, + + { + type: "Drehzahl", + name: "Dz", + signals: [ + { name: "DzOk", type: SignalType.DIGITAL_INPUT }, + { name: "DzImpuls", type: SignalType.DIGITAL_INPUT }, + ], + }, + + { + type: "Vorortbedienung", + name: "Vorort", + signals: [ + { name: "TaEin", type: SignalType.DIGITAL_INPUT }, + { name: "TaNichtAus", type: SignalType.DIGITAL_INPUT }, + ], + + }, + + { + type: "Überwachung", + name: "Ls", + signals: [ + { name: "MaxOk", type: SignalType.DIGITAL_INPUT }, + { name: "Min", type: SignalType.DIGITAL_INPUT }, + ], + + }, + { + type: "Fuellstand", + name: "Fuellstand", + signals: [ + { name: "Value", type: SignalType.ANALOG_INPUT }, + ], + + }, +] + +/* old + +// ============================================================ +// Properties pro ComponentType +// ============================================================ + +const componentProperties: Record = { + [ComponentTypes.UNKNOWN]: [], + + [ComponentTypes.ANTRIEB]: [ + { + name: "manufacturer", + label: "Hersteller", + type: "text", + }, + { + name: "power", + label: "Leistung [kW]", + type: "number", + }, + + ], + + [ComponentTypes.FREQUENZUMRICHTER]: [ + { + name: "manufacturer", + label: "Hersteller", + type: "text", + }, + { + name: "power", + label: "Leistung [kW]", + type: "number", + }, + { + name: "maxFrequency", + label: "Max. Frequenz [Hz]", + type: "number", + }, + ], + + [ComponentTypes.REPARATUR_SCHALTER]: [ + { + name: "manufacturer", + label: "Hersteller", + type: "text", + }, + { + name: "voltage", + label: "Spannung [V]", + type: "number", + }, + ], + + [ComponentTypes.LIMIT_SWITCH]: [ + { + name: "manufacturer", + label: "Hersteller", + type: "text", + }, + { + name: "normallyClosed", + label: "Öffner", + type: "boolean", + }, + ], + + [ComponentTypes.SCHIEFLAUF]: [ + { + name: "manufacturer", + label: "Hersteller", + type: "text", + }, + { + name: "sensitivity", + label: "Empfindlichkeit", + type: "number", + }, + ], + + [ComponentTypes.REISSLEINE]: [ + { + name: "manufacturer", + label: "Hersteller", + type: "text", + }, + { + name: "length", + label: "Seillänge [m]", + type: "number", + }, + ], + + [ComponentTypes.DREHZALHL]: [ + { + name: "manufacturer", + label: "Hersteller", + type: "text", + }, + { + name: "maxRpm", + label: "Max. Drehzahl [rpm]", + type: "number", + }, + ], + + [ComponentTypes.VORORT]: [ + { + name: "manufacturer", + label: "Hersteller", + type: "text", + }, + { + name: "buttonCount", + label: "Anzahl Taster", + type: "number", + }, + ], + + [ComponentTypes.UEBERWACHUNG]: [ + { + name: "manufacturer", + label: "Hersteller", + type: "text", + }, + { + name: "monitoringType", + label: "Überwachungstyp", + type: "select", + options: [ + { + value: "temperature", + label: "Temperatur", + }, + { + value: "pressure", + label: "Druck", + }, + { + value: "vibration", + label: "Vibration", + }, + ], + }, + ], +} + +*/ \ No newline at end of file diff --git a/src/actions/rk2/api-component.ts b/src/actions/rk2/api-component.ts new file mode 100644 index 0000000..3de1e3c --- /dev/null +++ b/src/actions/rk2/api-component.ts @@ -0,0 +1,134 @@ +'use server' + +import { Sites } from "@/_config/sites"; +import { ComponentCreate, ComponentOfUnit, ComponentResponse, ComponentUpdate } from "@/types/anlagen/components"; +import { UnitResponse } from "@/types/anlagen/units"; +import { revalidatePath } from "next/cache"; + +const url = "http://localhost:3000/rk2/"; +const url_component = url + "components" + + + +//----------------------------------------------------------------------------- +//--- Component by ID +//----------------------------------------------------------------------------- +export async function api_component_byId(id: string): Promise> { + + const result = await fetch(`${url_component}/${id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + + return { success: true, data:resultData as ComponentOfUnit }; + +} + + +//----------------------------------------------------------------------------- +//--- Component CREATE +//----------------------------------------------------------------------------- +export async function api_component_create(unitId:string,data: ComponentCreate): Promise> { + console.log(data); + const result = await fetch(`${url_component}/${unitId}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.rk2.units.url) + + return { success: true, data:resultData }; +} + + +//----------------------------------------------------------------------------- +//--- Component UPDATE +//----------------------------------------------------------------------------- +export async function api_component_update(id:string,data: ComponentUpdate): Promise> { + console.log('Update unit' + id); + const result = await fetch(`${url_component}/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.rk2.units.url + "/") + return { success: true, data:resultData }; +} + +//----------------------------------------------------------------------------- +//--- Component Delete +//----------------------------------------------------------------------------- +export async function api_component_remove(compId: string): Promise> { + + const result = await fetch(`${url_component}/${compId}`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' } + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.rk2.units.url); + return { success: true, data:resultData }; + +} + +//----------------------------------------------------------------------------- +//--- Component Delete +//----------------------------------------------------------------------------- +export async function api_componentPropertieDelete(compId: string,propName:string): Promise> { + + const result = await fetch(`url_component/${compId}/delteProp/${propName}`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' } + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.rk2.units.url); + return { success: true, data:resultData }; + +} \ No newline at end of file diff --git a/src/actions/rk2/api-groups.ts b/src/actions/rk2/api-groups.ts new file mode 100644 index 0000000..aff5ab2 --- /dev/null +++ b/src/actions/rk2/api-groups.ts @@ -0,0 +1,119 @@ +'use server' + +import { Sites } from "@/_config/sites"; +import { GroupCreate, GroupResponse } from "@/types/anlagen/groups"; +import { revalidatePath } from "next/cache"; + +const url = "http://localhost:3000/rk2/"; +const url_groups = url + "groups" + +//----------------------------------------------------------------------------- +//--- Groups +//----------------------------------------------------------------------------- + +export async function api_groups(): Promise { + + const result = await fetch(`${url_groups}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +//----------------------------------------------------------------------------- +//--- Group by ID +//----------------------------------------------------------------------------- +export async function api_groups_byId(id: string): Promise> { + + const result = await fetch(`${url_groups}/${id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + + return { success: true, data:resultData }; + +} + +//----------------------------------------------------------------------------- +//--- Groups CREATE +//----------------------------------------------------------------------------- +export async function api_groups_create(data: GroupCreate): Promise> { +console.log('Create group'); + const result = await fetch(`${url_groups}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.rk2.groups.url + "/") + return { success: true, data:resultData }; +} + +//----------------------------------------------------------------------------- +//--- Groups UPDATE +//----------------------------------------------------------------------------- +export async function api_groups_update(id:string,data: GroupCreate): Promise> { + console.log('Update group' + id); + const result = await fetch(`${url_groups}/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.rk2.groups.url) + return { success: true, data:resultData }; +} + +//----------------------------------------------------------------------------- +//--- Groups DELETE +//----------------------------------------------------------------------------- +export async function api_groups_delete(id:string): Promise> { + console.log('Update group' + id); + const result = await fetch(`${url_groups}/${id}`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.rk2.groups.url) + return { success: true, data:resultData }; +} diff --git a/src/actions/rk2/api-ios.ts b/src/actions/rk2/api-ios.ts new file mode 100644 index 0000000..51eda06 --- /dev/null +++ b/src/actions/rk2/api-ios.ts @@ -0,0 +1,97 @@ +'use server' + +import { Sites } from "@/_config/sites"; +import { IoCreate, IoResponse } from "@/types/anlagen/ios"; +import { revalidatePath } from "next/cache"; + +const url = "http://localhost:3000/rk2/"; +const url_ios = url + "ios" + +//----------------------------------------------------------------------------- +//--- ios +//----------------------------------------------------------------------------- + +export async function api_ios(): Promise { + + const result = await fetch(`${url_ios}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +//----------------------------------------------------------------------------- +//--- io by ID +//----------------------------------------------------------------------------- +export async function api_ios_byId(id: string): Promise> { + + const result = await fetch(`${url_ios}/${id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + + return { success: true, data:resultData }; + +} + +//----------------------------------------------------------------------------- +//--- ios CREATE +//----------------------------------------------------------------------------- +export async function api_ios_create(data: IoResponse): Promise> { + + const result = await fetch(`${url_ios}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.rk2.ios.url + "/") + return { success: true, data:resultData }; +} + +//----------------------------------------------------------------------------- +//--- ios UPDATE +//----------------------------------------------------------------------------- +export async function api_ios_update(id:string,data: IoCreate): Promise> { + + const result = await fetch(`${url_ios}/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.rk2.ios.url) + return { success: true, data:resultData }; +} + diff --git a/src/actions/rk2/api-signals.ts b/src/actions/rk2/api-signals.ts new file mode 100644 index 0000000..faff45f --- /dev/null +++ b/src/actions/rk2/api-signals.ts @@ -0,0 +1,112 @@ +'use server' + +import { Sites } from "@/_config/sites"; +import { SignalCreate, SignalResponse } from "@/types/anlagen/signals"; +import { revalidatePath } from "next/cache"; + +const url = "http://localhost:3000/rk2/"; +const url_signals = url + "signals" + +//----------------------------------------------------------------------------- +//--- Signals +//----------------------------------------------------------------------------- + +export async function api_signals(): Promise { + + const result = await fetch(`${url_signals}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +//----------------------------------------------------------------------------- +//--- Signal by ID +//----------------------------------------------------------------------------- +export async function api_signals_byId(id: string): Promise> { + + const result = await fetch(`${url_signals}/${id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + + return { success: true, data:resultData }; + +} + +//----------------------------------------------------------------------------- +//--- Signals by compId +//----------------------------------------------------------------------------- + +export async function api_signalsByComp(compId:string): Promise { + + const result = await fetch(`${url_signals}/byComponent/${compId}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} +//----------------------------------------------------------------------------- +//--- Signals CREATE +//----------------------------------------------------------------------------- +export async function api_signals_create(data: SignalCreate): Promise> { + + const result = await fetch(`${url_signals}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.rk2.signals.url + "/") + return { success: true, data:resultData }; +} + +//----------------------------------------------------------------------------- +//--- Signals UPDATE +//----------------------------------------------------------------------------- +export async function api_signals_update(id:string,data: SignalCreate): Promise> { + + const result = await fetch(`${url_signals}/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.rk2.signals.url) + return { success: true, data:resultData }; +} + diff --git a/src/actions/rk2/api-units.ts b/src/actions/rk2/api-units.ts new file mode 100644 index 0000000..a5c9e95 --- /dev/null +++ b/src/actions/rk2/api-units.ts @@ -0,0 +1,123 @@ +'use server' + +import { Sites } from "@/_config/sites"; +import { ComponentCreate, ComponentUpdate } from "@/types/anlagen/components"; +import { UnitCreate, UnitResponse } from "@/types/anlagen/units"; +import { revalidatePath } from "next/cache"; + +const url = "http://localhost:3000/rk2/"; +const url_units = url + "units" +const comp_update = "component" +const comp_add = "addComponent" +//----------------------------------------------------------------------------- +//--- Units +//----------------------------------------------------------------------------- + +export async function api_units(): Promise { + + const result = await fetch(`${url_units}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await result.json(); + + return data; +} + +//----------------------------------------------------------------------------- +//--- Units by ID +//----------------------------------------------------------------------------- +export async function api_units_byId(id: string): Promise> { + + const result = await fetch(`${url_units}/${id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + + return { success: true, data:resultData }; + +} + +//----------------------------------------------------------------------------- +//--- Units CREATE +//----------------------------------------------------------------------------- +export async function api_units_create(data: UnitCreate): Promise> { + + const result = await fetch(`${url_units}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.rk2.units.url + "/") + return { success: true, data:resultData }; +} + +//----------------------------------------------------------------------------- +//--- Units UPDATE +//----------------------------------------------------------------------------- +export async function api_units_update(id:string,data: UnitCreate): Promise> { + console.log('Update unit'); + console.log(data); + const result = await fetch(`${url_units}/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.rk2.units.url + "/") + return { success: true, data:resultData }; +} + +//----------------------------------------------------------------------------- +//--- Units UPDATE Component +//----------------------------------------------------------------------------- +export async function api_units_updateComponent(unitId: string, compId: string, data: ComponentUpdate): Promise> { + console.log('Create unit' ); + const result = await fetch(`${url_units}${unitId}/${comp_update}/${compId}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + const resultData = await result.json(); + + if (!result.ok) { + return { + success: false, + errors: resultData.message, // kommt aus NestJS + }; + } + + revalidatePath(Sites.rk2.units.url + "/") + return { success: true, data:resultData }; +} diff --git a/src/app/(pages)/doc/elemente/page.tsx b/src/app/(pages)/doc/elemente/page.tsx new file mode 100644 index 0000000..1a29c18 --- /dev/null +++ b/src/app/(pages)/doc/elemente/page.tsx @@ -0,0 +1,105 @@ + + + +export default async function PageElements() { + + return ( +
+
+
Gruppe
+
    +
  • G35
  • +
  • Name:Gurtförderr
  • +
  • Typ:Band
  • +
    +
    Elemente
    +
      +
    • +
      Name:
      +
      Motor
      +
    • +
    • Seilzug
    • +
    • Repschalter
    • +
    • Schieflauf
    • +
    • Vorortbedienung
    • +
    • Endschalter
    • +
    • FU
    • +
    • Drehzahl
    • +
    +
    +
+
+
+ ); +} + +/* +(Group)BA3 + (Group)H-Förderer + (Group)G35 Band + (Element) Motor 1 + (Element) Motor 2 + (Element) Drehzahl vorne + (Element) Reperaturschalter + (Element) Reissleine + (Element) Endschalter + (Element) Visu + +*/ + +interface Group { + id: number, + name: string, + nr: string, + container: boolean + groups?: Group[] +} + +interface Unit { + group: Group + typ: string, + name: string, + componentes: Componente[] +} + +type Componente = ComponenteMotor | ComponenteSchieflauf; + +interface ComponenteBase { + name: string, + typ: string, + + +} + +interface ComponenteMotor extends ComponenteBase { + motorEigenschaften: string //nur Platzhalter momentan + +} + +interface ComponenteSchieflauf { + name: string, + trueOK:boolean + +} + +const beispielBand: Unit = { + group: { + id: 1, + name: "BA4", + nr: '100', + container: true + }, + typ: "Band", + name: "Förderband zu 1234", + componentes: [{ + name: "Antrieb", + typ: "Motor", + motorEigenschaften: "platzhalter Beispiel" + }, + { + name: "Schieflauf", + typ: "Schieflauf", + trueOK: true + }, + ] +} \ No newline at end of file diff --git a/src/app/(pages)/procon/aggregate/[id]/@modal/(.)details/page.tsx b/src/app/(pages)/procon/aggregate/[id]/@modal/(.)details/page.tsx new file mode 100644 index 0000000..68a0ba5 --- /dev/null +++ b/src/app/(pages)/procon/aggregate/[id]/@modal/(.)details/page.tsx @@ -0,0 +1,28 @@ +"use client"; + +import Modal from "@/components/ui/Modal"; +import { useRouter } from "next/navigation"; + +interface Props{ + test:string +} + +export default function AggregateDetails({test}:Props) { + const router = useRouter(); + + return ( + + + +

Produktdetails

+ +

Hier stehen die detaillierten Informationen.

+ + + + +
+ ); +} \ No newline at end of file diff --git a/src/app/(pages)/procon/aggregate/[id]/@modal/default.tsx b/src/app/(pages)/procon/aggregate/[id]/@modal/default.tsx new file mode 100644 index 0000000..a4e72b1 --- /dev/null +++ b/src/app/(pages)/procon/aggregate/[id]/@modal/default.tsx @@ -0,0 +1,4 @@ + +export default function Default() { + return null +} \ No newline at end of file diff --git a/src/app/(pages)/procon/aggregate/[id]/details/page.tsx b/src/app/(pages)/procon/aggregate/[id]/details/page.tsx new file mode 100644 index 0000000..e45064d --- /dev/null +++ b/src/app/(pages)/procon/aggregate/[id]/details/page.tsx @@ -0,0 +1,18 @@ +export default async function DetailsPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + return ( +
+

Details für Produkt {id}

+ +

+ Diese Seite wird angezeigt, wenn man direkt + /produkte/{id}/details aufruft. +

+
+ ); +} \ No newline at end of file diff --git a/src/app/(pages)/procon/aggregate/[id]/layout.tsx b/src/app/(pages)/procon/aggregate/[id]/layout.tsx new file mode 100644 index 0000000..30a7a22 --- /dev/null +++ b/src/app/(pages)/procon/aggregate/[id]/layout.tsx @@ -0,0 +1,14 @@ +export default function Layout({ + children, + modal, +}: { + children: React.ReactNode; + modal: React.ReactNode; +}) { + return ( + <> + {children} + {modal} + + ); +} \ No newline at end of file diff --git a/src/app/(pages)/procon/aggregate/[id]/page.tsx b/src/app/(pages)/procon/aggregate/[id]/page.tsx new file mode 100644 index 0000000..7918f73 --- /dev/null +++ b/src/app/(pages)/procon/aggregate/[id]/page.tsx @@ -0,0 +1,74 @@ +import { Sites } from "@/_config/sites"; +import { api_ProconAggregate_GetById } from "@/actions/procon/api-procon-aggregate"; +import Visualisierungen from "@/components/procon/Visualsierungen"; + + +import Link from "next/link"; + +type Props = { + params: Promise<{ + id: string; + }>; +}; + +export default async function PageAggregateDetail({ params }: Props) { + const { id } = await params; + const data = await api_ProconAggregate_GetById(Number(id)); + + + return ( +
+ + {/* Überschrift Leiste */} +
+ {Sites.main.name} + {Sites.procon.aggregate.name} +
+ + {/* Aggregat Base Data */} +
+
+
Anlage :
+
{data.anlage}
+
+ +
+
Agg Nr :
+
{data.aggNr}
+
+ +
+
Bezeichnung :
+
{data.bezeichnung}
+
+ +
+
SPS Name :
+
{data.viewName}
+
+ +
+
BMK :
+
{data.bmk}
+
+
ss Modal
+ +
+ + {/* Visualisrung Elemente Anzeigen */} +
+ {data.visuElements ? + : +
Keine Daten
+ + } +
+ {false && +
+ {JSON.stringify(data, null, 2)} + {id} +
+ } +
+ ); +} diff --git a/src/app/(pages)/procon/aggregate/page.tsx b/src/app/(pages)/procon/aggregate/page.tsx new file mode 100644 index 0000000..b41d9d7 --- /dev/null +++ b/src/app/(pages)/procon/aggregate/page.tsx @@ -0,0 +1,40 @@ +import { Sites } from "@/_config/sites"; +import { api_ProconAggregate_GetAll } from "@/actions/procon/api-procon-aggregate"; + +import Link from "next/link"; + + +export default async function PageAggregate() { + const aggregate = await api_ProconAggregate_GetAll(); + return ( +
+ {Sites.main.name} + {Sites.procon.name} +
+ {aggregate.map((anlage) => ( +
+
+ {anlage.Id} +
+
+ {anlage.SPSAntriebId} +
+
+ {anlage.Bezeichnung} +
+ +
+ {anlage.BMK} +
+
+ {anlage.ViewName} +
+
+ Auswahl +
+
))} +
+
+ ); +} diff --git a/src/app/(pages)/procon/page.tsx b/src/app/(pages)/procon/page.tsx new file mode 100644 index 0000000..421bc78 --- /dev/null +++ b/src/app/(pages)/procon/page.tsx @@ -0,0 +1,30 @@ +import { Sites } from "@/_config/sites"; +import { api_ProconAggregate_GetAll } from "@/actions/procon/api-procon-aggregate"; + +import Link from "next/link"; + + +export default async function PageAggregate() { + + const topics = [ + { name: Sites.procon.aggregate.name, url: Sites.procon.aggregate.url }, + { name: Sites.procon.texteVisu.name, url: Sites.procon.texteVisu.url }, + { name: "Visu Texte",url: Sites.procon.texteVisu.url }, + { name: "SVG Test",url: Sites.procon.svgTest.url } + ] + + + return ( +
+ {Sites.main.name} +
+ {topics.map((top) => ( + + + {top.name} + + ))} +
+
+ ); +} diff --git a/src/app/(pages)/procon/svg/page.tsx b/src/app/(pages)/procon/svg/page.tsx new file mode 100644 index 0000000..95744ea --- /dev/null +++ b/src/app/(pages)/procon/svg/page.tsx @@ -0,0 +1,52 @@ +"use client" +import { Sites } from "@/_config/sites"; +import { api_ProconAggregate_GetAll } from "@/actions/procon/api-procon-aggregate"; +import SvgView from "@/components/procon/svg/SvgView"; +import { xamlToSvg } from "@/components/procon/svg/xaml_to_svg"; + +import Link from "next/link"; +import { useEffect, useState } from "react"; + +const x = ` + + + + + + + + + +` +export default function PageSVG() { + + const [xaml, setXaml] = useState(x); + + const getContent = () => { + try { + return ; + } catch (error) { + + + console.log('Unbekannter fehler') + + } + }; + + return ( +
+ + { + getContent() + } + +
+ ); +} diff --git a/src/app/(pages)/rk/anlagen/page.tsx b/src/app/(pages)/rk/anlagen/page.tsx new file mode 100644 index 0000000..4f73e4f --- /dev/null +++ b/src/app/(pages)/rk/anlagen/page.tsx @@ -0,0 +1,23 @@ + +import { Sites } from "@/_config/sites"; +import { api_anlage_getSelected, api_anlage_setSelected, api_anlagen_getAll } from "@/actions/rk/api-anlage"; +import AnlageColumn from "@/components/rk/anlage/anlageColumn"; +import Link from "next/link"; + + +export default async function PageAnlagen() { + const anlagen = await api_anlagen_getAll(); + const currentAnlage = await api_anlage_getSelected(); + + return ( +
+ {Sites.main.name} +
+ {anlagen.map((anlage) => + + )} + +
+
+ ) +} diff --git a/src/app/(pages)/rk/elements/new/page.tsx b/src/app/(pages)/rk/elements/new/page.tsx new file mode 100644 index 0000000..8f60b0a --- /dev/null +++ b/src/app/(pages)/rk/elements/new/page.tsx @@ -0,0 +1,29 @@ +import { Sites } from "@/_config/sites"; +import { api_anlage_getSelected } from "@/actions/rk/api-anlage"; +import { api_ElementsGet } from "@/actions/rk/api-element"; +import { api_GroupAll } from "@/actions/rk/api-groups"; +import ElementCreateView from "@/components/rk/elements/elementCreateView"; + + +import Link from "next/link"; + +export default async function PageElementNew() { + const groups = await api_GroupAll(); + const currentAnlage = await api_anlage_getSelected() + const elements = await api_ElementsGet(); + return ( +
+
+
+ {Sites.elements.name} +
+
+ {Sites.main.name} +
+
+ + + +
+ ); +} diff --git a/src/app/(pages)/rk/elements/page.tsx b/src/app/(pages)/rk/elements/page.tsx new file mode 100644 index 0000000..361a397 --- /dev/null +++ b/src/app/(pages)/rk/elements/page.tsx @@ -0,0 +1,29 @@ +import { Sites } from "@/_config/sites"; +import { api_ElementsGet } from "@/actions/rk/api-element"; +import { api_GroupAll } from "@/actions/rk/api-groups"; +import ElementView from "@/components/rk/elements/elementsView"; + + +import Link from "next/link"; + +export default async function PageElements() { + const elements = await api_ElementsGet(); + const groups = await api_GroupAll(); + + return ( +
+
+
+ {Sites.main.name} +
+
+ {Sites.elements.new.name} +
+
+ + + + +
+ ); +} diff --git a/src/app/(pages)/rk/groups/new/page.tsx b/src/app/(pages)/rk/groups/new/page.tsx new file mode 100644 index 0000000..827a9e8 --- /dev/null +++ b/src/app/(pages)/rk/groups/new/page.tsx @@ -0,0 +1,28 @@ +import { Sites } from "@/_config/sites"; +import { api_anlage_getSelected } from "@/actions/rk/api-anlage"; +import { api_GroupAll } from "@/actions/rk/api-groups"; +import { GroupEdit } from "@/components/rk/groups/group_Edit"; +import { GroupEditList } from "@/components/rk/groups/group_edit_list"; +import GroupView from "@/components/rk/groups/groupView"; + + +import Link from "next/link"; + +export default async function PageGroupNew() { + const groups = await api_GroupAll(); + const anlage = await api_anlage_getSelected() + + return ( +
+
+
+ {Sites.groups.name} +
+
+ + {Sites.main.name} + {/**/} + +
+ ); +} diff --git a/src/app/(pages)/rk/groups/page.tsx b/src/app/(pages)/rk/groups/page.tsx new file mode 100644 index 0000000..a04cf50 --- /dev/null +++ b/src/app/(pages)/rk/groups/page.tsx @@ -0,0 +1,26 @@ +import { Sites } from "@/_config/sites"; +import { api_GroupAll } from "@/actions/rk/api-groups"; +import GroupView from "@/components/rk/groups/groupView"; + + +import Link from "next/link"; + +export default async function PageGroups() { + const groups = await api_GroupAll(); + + return ( +
+ +
+
+ {Sites.main.name} +
+
+ {Sites.groups.new.name} +
+
+ + +
+ ); +} diff --git a/src/app/(pages)/rk/log-note/page.tsx b/src/app/(pages)/rk/log-note/page.tsx new file mode 100644 index 0000000..0084e1c --- /dev/null +++ b/src/app/(pages)/rk/log-note/page.tsx @@ -0,0 +1,28 @@ +import { Sites } from "@/_config/sites"; +import { api_GroupAll } from "@/actions/rk/api-groups"; +import { api_LogNoteGet } from "@/actions/rk/api-logNote"; +import GroupView from "@/components/rk/groups/groupView"; +import LogNoteView from "@/components/rk/logNote/logNoteView"; + + +import Link from "next/link"; + +export default async function PageGroups() { + const notes = await api_LogNoteGet(); + + return ( +
+ +
+
+ {Sites.main.name} +
+
+ {Sites.logNote.new.name} +
+
+ + +
+ ); +} diff --git a/src/app/(pages)/rk/peripheries/differences/page.tsx b/src/app/(pages)/rk/peripheries/differences/page.tsx new file mode 100644 index 0000000..aaa4a5d --- /dev/null +++ b/src/app/(pages)/rk/peripheries/differences/page.tsx @@ -0,0 +1,46 @@ +import { Sites } from "@/_config/sites"; +import { PeripherieFindApi, GetEA_Differences } from "@/actions/rk/api-peripherie"; +import Link from "next/link"; + +export default async function PagePeripherie() { + const peripheries = await GetEA_Differences(); + + return ( +
+
+
+ + E/A Unterschiede +
+
+
+
+ {Sites.main.name} + +
+
+ {Sites.peripheries.name} + +
+ +
+ + + {peripheries.map((p) => (
+
+ {p.adrType}{p.adrByte}.{p.adrBit} +
+ +
+ {p.text} +
+
+ {p.excelData?.functionText} +
+ +
))} + + +
+ ); +} diff --git a/src/app/(pages)/rk/peripheries/page.tsx b/src/app/(pages)/rk/peripheries/page.tsx new file mode 100644 index 0000000..2dc3c5f --- /dev/null +++ b/src/app/(pages)/rk/peripheries/page.tsx @@ -0,0 +1,37 @@ +import { Sites } from "@/_config/sites"; +import { api_ElementsGet } from "@/actions/rk/api-element"; +import { api_GroupAll } from "@/actions/rk/api-groups"; +import { PeripherieFindApi } from "@/actions/rk/api-peripherie"; +import PeripherieListView from "@/components/rk/peripherie/peripherieListView"; + +import Link from "next/link"; + + +export default async function PagePeripherie() { + const peripheries = await PeripherieFindApi(); //{ groupId: "exists" } + const groups = await api_GroupAll(); + const elmente = await api_ElementsGet(); + + + + return ( +
+
+
+ {Sites.main.name} + +
+
+ {Sites.peripheries.differences.name} + +
+ +
+ + + + + +
+ ); +} diff --git a/src/app/(pages)/rk2/groups/edit/[id]/page.tsx b/src/app/(pages)/rk2/groups/edit/[id]/page.tsx new file mode 100644 index 0000000..e87cfef --- /dev/null +++ b/src/app/(pages)/rk2/groups/edit/[id]/page.tsx @@ -0,0 +1,50 @@ + +import { Sites } from "@/_config/sites"; +import { api_units_byId } from "@/actions/rk2/api-units"; + +import { api_anlage_getSelected } from "@/actions/rk/api-anlage"; +import { UnitCreateForm } from "@/components/rk2/units/unit_createForm"; + +import Link from "next/link"; +import { UnitEdit } from "@/components/rk2/units/unitEdit"; +import { api_groups, api_groups_byId } from "@/actions/rk2/api-groups"; +import { componentDefaults, componentTypeDefinitions } from "@/actions/rk2/api-compTypes"; + +import Breadcrumbs from "@/components/ui/navigation/breadcrumbs"; +import { GroupForm } from "@/components/rk2/groups/groupForm"; +import { GroupEdit } from "@/components/rk2/groups/groupEdit"; + +type Props = { + params: Promise<{ + id: string; + }>; +}; + + + +export default async function PageGroupEdit({ params }: Props) { + + const { id } = await params; + const resultFindGroupById = await api_groups_byId(id); + const anlage = await api_anlage_getSelected(); + const group = resultFindGroupById.data; + const groups = await api_groups(); + + const breadcrumb = [{ label: "RK", href: "/" },{ label: "Anlage", href: "/rk2" },{ label: "Groups", href: "/rk2/groups" }, { label: "Edit" }, { label: group.name }] + + if (!resultFindGroupById.success) return
Grouppe nicht gefunden {resultFindGroupById.errors} {id}
+ + return ( +
+
+ +
+ +
+ + + +
+
+ ); +} diff --git a/src/app/(pages)/rk2/groups/edit/new/page.tsx b/src/app/(pages)/rk2/groups/edit/new/page.tsx new file mode 100644 index 0000000..5c94b04 --- /dev/null +++ b/src/app/(pages)/rk2/groups/edit/new/page.tsx @@ -0,0 +1,30 @@ +import { Sites } from "@/_config/sites"; +import { api_groups } from "@/actions/rk2/api-groups"; +import { api_anlage_getSelected } from "@/actions/rk/api-anlage"; +import { GroupEdit } from "@/components/rk2/groups/groupEdit"; + + + + +import Link from "next/link"; +import Breadcrumbs from "@/components/ui/navigation/breadcrumbs"; + + +export default async function PageGroups() { + const currentAnlage= await api_anlage_getSelected(); + const groups = await api_groups(); + + const breadcrumb = [{ label: "RK", href: "/" },{ label: "Anlage", href: "/rk2" },{ label: "Groups", href: "/rk2/groups" }, { label: "New" }] + + + return ( +
+
+ +
+
+ +
+
+ ); +} diff --git a/src/app/(pages)/rk2/groups/page.tsx b/src/app/(pages)/rk2/groups/page.tsx new file mode 100644 index 0000000..4220d70 --- /dev/null +++ b/src/app/(pages)/rk2/groups/page.tsx @@ -0,0 +1,29 @@ +import { Sites } from "@/_config/sites"; +import { api_groups } from "@/actions/rk2/api-groups"; +import { api_anlage_getSelected } from "@/actions/rk/api-anlage"; +import { GroupViewList } from "@/components/rk2/groups/groupViewList"; + + + +import Link from "next/link"; +import Breadcrumbs from "@/components/ui/navigation/breadcrumbs"; + + +export default async function PageGroups() { + const currentAnlage= await api_anlage_getSelected(); + const groups = await api_groups(); + + const breadcrumb = [{ label: "RK", href: "/" },{ label: "Anlage", href: "/rk2" },{ label: "Groups" }] + + + return ( +
+
+ +
+
+ +
+
+ ); +} diff --git a/src/app/(pages)/rk2/layout.tsx b/src/app/(pages)/rk2/layout.tsx new file mode 100644 index 0000000..df60946 --- /dev/null +++ b/src/app/(pages)/rk2/layout.tsx @@ -0,0 +1,55 @@ +"use client" + +import { api_anlage_getSelected } from "@/actions/rk/api-anlage"; +import { Anlage } from "@/types/rk"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; + +interface Props { + children: React.ReactNode +} + +export default function AnlageLayout({ children }: Props) { + const router = useRouter(); + const [anlage,setAnlage] = useState() + + useEffect(()=>{ + const loadAnlage = async ()=>{ + const currentAnlage = await api_anlage_getSelected(); + setAnlage(()=>currentAnlage); + } + loadAnlage(); + },[]) + + return ( +
+ +
+

+ {`RK -${anlage?.anlageNr} ${anlage?.name}`} +

+
+
+ + + {/* Content */} +
+ {children} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/(pages)/rk2/page.tsx b/src/app/(pages)/rk2/page.tsx new file mode 100644 index 0000000..cbdb954 --- /dev/null +++ b/src/app/(pages)/rk2/page.tsx @@ -0,0 +1,51 @@ +import { Sites } from "@/_config/sites"; +import { api_units } from "@/actions/rk2/api-units"; + +import { api_anlage_getSelected } from "@/actions/rk/api-anlage"; + + +import Link from "next/link"; +import { List, ListItem, ListItemAction } from "@/components/ui/list"; +import { Button } from "@/components/ui/button"; +import Breadcrumbs from "@/components/ui/navigation/breadcrumbs"; + + + +export default async function PageUnits() { + + const currentAnlage = await api_anlage_getSelected(); + const units = await api_units(); + const breadcrumb = [{ label: "RK", href: "/" },{ label: "Anlage" }] + + return ( + <> + {/* Main-Nav */} + + +
+ + + {units.map((unit) => ( + +
+ {unit.name} +
+
+ {unit.type} +
+
+ {unit.components?.map((comp) =>
{comp.name}
)} +
+ + + +
+ + ))} +
+
+ + ); +} diff --git a/src/app/(pages)/rk2/signals/[componentId]/page.tsx b/src/app/(pages)/rk2/signals/[componentId]/page.tsx new file mode 100644 index 0000000..90bbf43 --- /dev/null +++ b/src/app/(pages)/rk2/signals/[componentId]/page.tsx @@ -0,0 +1,47 @@ +import { Sites } from "@/_config/sites"; +import { api_anlage_getSelected } from "@/actions/rk/api-anlage"; +import { api_component_byId } from "@/actions/rk2/api-component"; +import { componentTypeDefinitions } from "@/actions/rk2/api-compTypes"; +import { SignalForm } from "@/components/rk2/signals/signalForm"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Link } from "lucide-react"; + +type Props = { + params: Promise<{ + componentId: string; + }>; +}; + + + +export default async function PageSignalsEdit({ params }: Props) { + + const { componentId } = await params; + const resultComponentOfId = await api_component_byId(componentId); + const currentAnlage= await api_anlage_getSelected(); + const componentTypes = componentTypeDefinitions; + + if(!resultComponentOfId) return
Keine Daten
+ return ( +
+ {Sites.main.name} + {Sites.procon.name} +
+ + + + {true && + + Debug + + {componentId} +
{JSON.stringify(resultComponentOfId, null, 2)}
+ +
+
+ } + +
+
+ ); +} diff --git a/src/app/(pages)/rk2/units/edit/[id]/page.tsx b/src/app/(pages)/rk2/units/edit/[id]/page.tsx new file mode 100644 index 0000000..6a35130 --- /dev/null +++ b/src/app/(pages)/rk2/units/edit/[id]/page.tsx @@ -0,0 +1,46 @@ +import { Sites } from "@/_config/sites"; +import { api_units_byId } from "@/actions/rk2/api-units"; + +import { api_anlage_getSelected } from "@/actions/rk/api-anlage"; +import { UnitCreateForm } from "@/components/rk2/units/unit_createForm"; + +import Link from "next/link"; +import { UnitEdit } from "@/components/rk2/units/unitEdit"; +import { api_groups } from "@/actions/rk2/api-groups"; +import { componentDefaults, componentTypeDefinitions } from "@/actions/rk2/api-compTypes"; +import Breadcrumbs from "@/components/ui/navigation/breadcrumbs"; + +type Props = { + params: Promise<{ + id: string; + }>; +}; + + + +export default async function PageUnitsEdit({ params }: Props) { + + const { id } = await params; + const resultFindUnitById = await api_units_byId(id); + const currentAnlage= await api_anlage_getSelected(); + const groups = await api_groups(); + const componentTypes = componentTypeDefinitions; + const cd = componentDefaults; + if(!resultFindUnitById.success) return
Unit nicht gefunden {resultFindUnitById.errors}
+ + const breadcrumb = [{ label: "RK", href: "/" },{ label: "Anlage", href: "/rk2" },{ label: "Units",href: "/rk2/units" },{ label: "Ändern" },{ label: resultFindUnitById.data.name }] + + return ( +
+
+ +
+ +
+ + + +
+
+ ); +} diff --git a/src/app/(pages)/rk2/units/edit/new/page.tsx b/src/app/(pages)/rk2/units/edit/new/page.tsx new file mode 100644 index 0000000..e24f3b9 --- /dev/null +++ b/src/app/(pages)/rk2/units/edit/new/page.tsx @@ -0,0 +1,34 @@ +import { Sites } from "@/_config/sites"; +import { api_groups } from "@/actions/rk2/api-groups"; +import { api_anlage_getSelected } from "@/actions/rk/api-anlage"; +import { UnitEdit } from "@/components/rk2/units/unitEdit"; + +import Link from "next/link"; +import { componentDefaults, componentTypeDefinitions } from "@/actions/rk2/api-compTypes"; +import Breadcrumbs from "@/components/ui/navigation/breadcrumbs"; + + + +export default async function PageUnitsEditNew() { + const currentAnlage = await api_anlage_getSelected(); + const groups = await api_groups(); + const componentTypes = componentTypeDefinitions; + const cd = componentDefaults; + + const breadcrumb = [{ label: "RK", href: "/" },{ label: "Anlage", href: "/rk2" },{ label: "Units" },{ label: "Neu" }] + + return ( +
+
+ +
+ +
+ + {/* + + */} +
+
+ ); +} diff --git a/src/app/(pages)/rk2/units/page.tsx b/src/app/(pages)/rk2/units/page.tsx new file mode 100644 index 0000000..41936e4 --- /dev/null +++ b/src/app/(pages)/rk2/units/page.tsx @@ -0,0 +1,83 @@ +import { Sites } from "@/_config/sites"; +import { api_units } from "@/actions/rk2/api-units"; +import { api_anlage_getSelected } from "@/actions/rk/api-anlage"; +import Link from "next/link"; +import { List, ListHeader, ListHeaderItem, ListItem, ListItemAction, ListItemContent, ListItemDescription, ListItemTitle } from "@/components/ui/list"; +import { Button } from "@/components/ui/button"; +import Breadcrumbs from "@/components/ui/navigation/breadcrumbs"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { GroupPath } from "@/components/rk2/groups/groupPath"; +import { api_groups } from "@/actions/rk2/api-groups"; +import { AusgabeUnitInfo } from "@/components/rk2/ausgabe/ausgabeUnitInfo"; + + + +export default async function PageUnits() { + + const units = await api_units(); + const groups = await api_groups(); + const breadcrumb = [{ label: "RK", href: "/" }, { label: "Anlage", href: "/rk2" }, { label: "Units" }] + + return ( + <> + {/* Main-Nav */} +
+ +
+ + + Units + + + + + + + Gruppe + Unit + Typ + + {units.map((unit) => ( + + + +
+ +
+
+ {unit.prefix}_{unit.bmk}_{unit.name}{unit.postfix ? "_"+unit.postfix:""} +
+
+ {unit.type} +
+
+ + {unit.components?.map((comp) =>
+ {comp.prefix && comp.prefix} + {comp.name} + {comp.postfix && comp.postfix} + {comp.index && comp.index>0 ? comp.index:""} +
)} +
+
+ + + +
+ + ))} +
+
+ + Ausgabe + + + + + + ); +} diff --git a/src/app/(pages)/test/page.tsx b/src/app/(pages)/test/page.tsx new file mode 100644 index 0000000..d6029c8 --- /dev/null +++ b/src/app/(pages)/test/page.tsx @@ -0,0 +1,142 @@ +"use client"; + +import { DndContext, closestCenter, DragOverlay, useDroppable } from "@dnd-kit/core"; +import { + arrayMove, + SortableContext, + useSortable, + verticalListSortingStrategy, + +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { useState } from "react"; +import type { DragEndEvent, DragStartEvent } from "@dnd-kit/core"; +import { SortableItem } from "@/components/test_dnd"; +import { DroppableContainer } from "@/components/ui/droppableContainer"; + +interface TestItem { + id: string, + name: string +} + +export default function Page() { + const [left, setLeft] = useState([{ id: "1", name: "Apfel" }, { id: "2", name: "Banane" }]); + const [right, setRight] = useState([{ id: "3", name: "Traube" }]); + + const [activeItem, setActiveItem] = useState(null); + + function findContainer(id: string) { + if (left.find((item) => item.id === id)) return "left"; + if (right.find((item) => item.id === id)) return "right"; + return null; + } + + function handleDragStart(event: DragStartEvent) { + const { active } = event; + + const item = + left.find((i) => i.id === active.id) || + right.find((i) => i.id === active.id); + + if (item) setActiveItem(item); + } + + function handleDragEnd(event: DragEndEvent) { + + const { active, over } = event; + setActiveItem(null); + if (!over) return; + + const overId = over.id; + + const activeContainer = findContainer(active.id as string); + + const overContainer = + overId === "left" || overId === "right" + ? overId + : findContainer(overId as string); + + + + + if (!activeContainer || !overContainer) return; + + // 🔁 Innerhalb gleicher Liste sortieren + if (activeContainer === overContainer) { + if (activeContainer === "left") { + const oldIndex = left.findIndex((i) => i.id === active.id); + const newIndex = left.findIndex((i) => i.id === over.id); + + setLeft(arrayMove(left, oldIndex, newIndex)); + } + + if (activeContainer === "right") { + const oldIndex = right.findIndex((i) => i.id === active.id); + const newIndex = right.findIndex((i) => i.id === over.id); + + setRight(arrayMove(right, oldIndex, newIndex)); + } + + } else { + if (activeContainer === "left") { + const item = left.find((i) => i.id === active.id); + if (!item) return; + + setLeft(left.filter((i) => i.id !== active.id)); + setRight([...right, item]); + } + + if (activeContainer === "right") { + const item = right.find((i) => i.id === active.id); + if (!item) return; + + setRight(right.filter((i) => i.id !== active.id)); + setLeft([...left, item]); + } + } + } + + return ( +
+ +
+ + i.id)} strategy={verticalListSortingStrategy}> +
+

Links

+ {left.map((item) => ( +
Test
+ ))} +
+
+
+ + + i.id)} strategy={verticalListSortingStrategy}> +
+

Rechts

+ {right.map((item) => ( +
Test
+ ))} +
+
+
+
+ {/* DRAG OVERLAY */} + + {activeItem ? ( +
+ {activeItem.id} {activeItem.name} +
+ ) : null} +
+
+
+ ); +} diff --git a/src/app/(pages)/upload/page.tsx b/src/app/(pages)/upload/page.tsx new file mode 100644 index 0000000..aa872cc --- /dev/null +++ b/src/app/(pages)/upload/page.tsx @@ -0,0 +1,10 @@ +import { uploadFile } from "@/actions/rk/api-file-uplaod"; + +export default function UploadPage() { + return ( +
+ + +
+ ); +} \ No newline at end of file diff --git a/src/app/(pages)/visu/create/page.tsx b/src/app/(pages)/visu/create/page.tsx new file mode 100644 index 0000000..626589b --- /dev/null +++ b/src/app/(pages)/visu/create/page.tsx @@ -0,0 +1,16 @@ +import { drawGetSvg } from "@/actions/rk/api-draw"; +import { api_VisuElementsAll } from "@/actions/rk/api-visuElements"; +import VisuCreatePlatform from "@/components/visu/create/visuCreatePlatform"; +import VisuContent from "@/components/visu/visuContent"; + +export default async function PageVisu() { + + + + + return ( +
+ +
+ ); +} diff --git a/src/app/(pages)/visu/elements/page.tsx b/src/app/(pages)/visu/elements/page.tsx new file mode 100644 index 0000000..3dd7250 --- /dev/null +++ b/src/app/(pages)/visu/elements/page.tsx @@ -0,0 +1,21 @@ + +import { api_ElementsGet } from "@/actions/rk/api-element"; +import { api_VisuElementsAll } from "@/actions/rk/api-visuElements"; +import VisuElementsList from "@/components/visu/visuElements/visuElementsList"; + + +export default async function PageVisuElemente() { + + const visuElements = await api_VisuElementsAll(); + const elements = await api_ElementsGet(); + + if(!visuElements) { + return
Loading...
+ } + + return ( +
+ +
+ ); +} diff --git a/src/app/(pages)/visu/groups_add/page.tsx b/src/app/(pages)/visu/groups_add/page.tsx new file mode 100644 index 0000000..ecc431f --- /dev/null +++ b/src/app/(pages)/visu/groups_add/page.tsx @@ -0,0 +1,28 @@ +import { Sites } from "@/_config/sites"; +import { api_ElementsGet } from "@/actions/rk/api-element"; +import { api_GroupAll } from "@/actions/rk/api-groups"; +import { api_VisuElementsAll } from "@/actions/rk/api-visuElements"; +import VisuAddGroups from "@/components/visu/visuAddGroups"; + + + +import Link from "next/link"; + + +export default async function PageGroups() { + const groups = await api_GroupAll(); + const elements = await api_ElementsGet(); + const visuElements = await api_VisuElementsAll(); + + if(!groups || !visuElements) return
Loading...
+ + return ( +
+ {Sites.main.name} +
+ +
+ +
+ ); +} diff --git a/src/app/(pages)/visu/page.tsx b/src/app/(pages)/visu/page.tsx new file mode 100644 index 0000000..612d274 --- /dev/null +++ b/src/app/(pages)/visu/page.tsx @@ -0,0 +1,22 @@ +import { VisuElementsProvider } from "@/_context/visu/VisuElementsProvider"; +import { drawGetSvg } from "@/actions/rk/api-draw"; +import { api_VisuElementsAll } from "@/actions/rk/api-visuElements"; +import VisuContent from "@/components/visu/visuContent"; + +export default async function PageVisu() { + + const visuElements = await api_VisuElementsAll(); + console.log('reander') + + if(!visuElements) { + console.log('reload!!!') + return
Loading...
+ } + return ( +
+ + + +
+ ); +} diff --git a/src/app/(pages)/visu/showDraw/page.tsx b/src/app/(pages)/visu/showDraw/page.tsx new file mode 100644 index 0000000..bfba5e5 --- /dev/null +++ b/src/app/(pages)/visu/showDraw/page.tsx @@ -0,0 +1,39 @@ +import { VisuElementsProvider } from "@/_context/visu/VisuElementsProvider"; +import { api_drawData } from "@/actions/draw/api-draw"; +import { drawGetSvg } from "@/actions/rk/api-draw"; +import { api_VisuElementsAll } from "@/actions/rk/api-visuElements"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { SvgShow } from "@/components/visu/draw/svgShow"; +import VisuContent from "@/components/visu/visuContent"; + +export default async function PageShowDraw() { + + const data = await api_drawData(); + + + + return ( +
+ + + + + + + + {true && + + Debug + +
{JSON.stringify(data, null, 2)}
+ +
+
+ } +
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..7ecd8eb 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,32 +3,31 @@ import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], + variable: "--font-geist-sans", + subsets: ["latin"], }); const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], + variable: "--font-geist-mono", + subsets: ["latin"], }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Create Next App", + description: "Generated by create next app", }; export default function RootLayout({ - children, + children, }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode; }>) { - return ( - - - {children} - - - ); + return ( + + + + {children} + + + ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 295f8fd..976cfad 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,65 +1,33 @@ -import Image from "next/image"; +import { Sites } from "@/_config/sites"; -export default function Home() { - return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
- -
-
- ); +import Image from "next/image"; +import Link from "next/link"; + +export default async function Home() { + + return ( +
+ +
+ {Sites.anlagen.name} + {Sites.groups.name} + {Sites.elements.name} + {Sites.peripheries.name} + {Sites.visu.name} + {Sites.visu.create.name} + {Sites.visu.selectGroups.name} + {Sites.visu.elements.name} + {Sites.logNote.name} + + {Sites.logNote.name} + +
+ +
+ + {Sites.rk2.name} + +
+
+ ); } diff --git a/src/components/procon/Visualsierungen.tsx b/src/components/procon/Visualsierungen.tsx new file mode 100644 index 0000000..9e40498 --- /dev/null +++ b/src/components/procon/Visualsierungen.tsx @@ -0,0 +1,119 @@ +"use client" + +import { ProConView, ProConXamlData } from "@/types/procon" +import { useEffect, useState } from "react"; +import VisualisierungPositionen from "./visualiserungPositionen"; +import { api_ProconViews_UpdateXAML } from "@/actions/procon/api-procon-views"; +import { useVisuAgg } from "@/hooks/procon/useVisuAgg"; +import SvgView from "./svg/SvgView"; + + +interface Props { + visuElements: ProConView[] +} + + +export default function Visualisierungen({ visuElements }: Props) { + const [selectedElement, setSelectedElement] = useState(); + const [selectedView, setSelectedView] = useState(); + + const { elements, viewNames, selectedViewElement, onSelectViewElement, graphics } = useVisuAgg({ data: visuElements }); + + + useEffect(() => { + setSelectedElement(undefined); + setSelectedView(undefined); + }, [visuElements]) + + + const headers = ['Symbol', "PosX", "PosY", "Höhe", "Breite", "Ebene", "Auswahl"] + + const handlerSaveXaml = () => { + if (!selectedView) return; + api_ProconViews_UpdateXAML(selectedView.id, selectedView.xaml) + } + + return ( +
+
+
+
+ XAML-Views +
+
+ {viewNames.map((v) => ( +
onSelectViewElement(v.index)} + > + {v.name} +
+ ))} +
+ +
+ +
+
+ {selectedViewElement?.viewType} +
+
X
+
{selectedViewElement?.posX}
+ +
+
+ {graphics.map((g) => (
{g.viewType}-{g.viewIndex}-{g.state}
))} +
+
+ {graphics.map((g) => ) + + + } + +
+
+ { graphics[0] && + +
+ + )} +
+ ); +} \ No newline at end of file diff --git a/src/components/procon/editXaml.tsx b/src/components/procon/editXaml.tsx new file mode 100644 index 0000000..75f6ad1 --- /dev/null +++ b/src/components/procon/editXaml.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Modal from "../ui/Modal"; +import { InputNumber } from "../ui/InputNumber"; +import { api_ProconVisualisierungen_Update } from "@/actions/procon/api-procon-visualisierungen"; +import { ProConView } from "@/types/procon"; + +interface Positionen{ + +} + +interface Props { + data:ProConView +} + + +export default function EditXaml({ data }: Props) { + const [showDetails, setShowDetails] = useState(false); + const [text, setText] = useState(''); + + const handleChange = (name:string,value:number)=>{ + console.log('Submit','name',name,' value',value); + let data :Partial = {} + if(name==='posX') data['posX'] = value; + if(name==='posY') data['posY'] = value; + if(name==='height') data['height'] = value; + if(name==='width') data['width'] = value; + if(name==='zIndex') data['zIndex'] = value; + + + //api_ProconVisualisierungen_Update(id,data); + } + + return ( +
+ {data.views.map((view)=>
{view.state}
)} + + +
+ ); +} + +/* +
{element.posX}
+
{element.posY}
+
{element.height}
+
{element.width}
+
{element.zIndex}
+ */ \ No newline at end of file diff --git a/src/components/procon/svg/SvgView.tsx b/src/components/procon/svg/SvgView.tsx new file mode 100644 index 0000000..4d5724b --- /dev/null +++ b/src/components/procon/svg/SvgView.tsx @@ -0,0 +1,15 @@ +'use client'; + +interface Props { + svg: string; +} + +export default function SvgView({ svg }: Props) { + return ( +
+ ); +} \ No newline at end of file diff --git a/src/components/procon/svg/page.tsx b/src/components/procon/svg/page.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/procon/svg/xaml_to_svg copy.ts b/src/components/procon/svg/xaml_to_svg copy.ts new file mode 100644 index 0000000..3a66071 --- /dev/null +++ b/src/components/procon/svg/xaml_to_svg copy.ts @@ -0,0 +1,1755 @@ +import { + DOMParser, + type Element as XmlElement, +} from '@xmldom/xmldom'; + +export interface XamlToSvgOptions { + pretty?: boolean; + width?: number | string; + height?: number | string; + viewBox?: string; +} + +interface XmlNode { + name: string; + attributes: Record; + children: XmlNode[]; + text?: string; +} + +interface Matrix { + a: number; + b: number; + c: number; + d: number; + e: number; + f: number; +} + +interface Point { + x: number; + y: number; +} + +interface Bounds { + x: number; + y: number; + width: number; + height: number; +} + +export class XamlToSvgError extends Error { + public readonly code: string; + + constructor( + message: string, + code = 'XAML_ERROR', + ) { + super(message); + + this.name = + 'XamlToSvgError'; + + this.code = code; + } +} +/** + * XAML -> SVG + * + * Unterstützt: + * + * Viewbox + * Canvas + * Path + * Ellipse + * Rectangle + * Line + * + * Canvas.Left + * Canvas.Top + * + * RenderTransformOrigin + * + * TranslateTransform + * RotateTransform + * ScaleTransform + * SkewTransform + * MatrixTransform + * TransformGroup + * + * Fill + * Stroke + * StrokeThickness + * Opacity + * Visibility + */ +export function xamlToSvg1( + xaml: string, + options: XamlToSvgOptions = {}, +): string { + const root = parseXml(xaml); + + const canvas = findFirst(root, 'Canvas'); + + if (!canvas) { + throw new Error( + 'Kein Canvas im XAML gefunden.', + ); + } + + const canvasWidth = + number(attr(canvas, 'Width')) ?? 100; + + const canvasHeight = + number(attr(canvas, 'Height')) ?? 100; + + const width = + options.width ?? + canvasWidth; + + const height = + options.height ?? + canvasHeight; + + const viewBox = + options.viewBox ?? + `0 0 ${format(canvasWidth)} ${format(canvasHeight)}`; + + const content = renderNode( + canvas, + identity(), + ); + + if (options.pretty) { + return [ + ``, + indentXml(content), + ``, + ].join('\n'); + } + + return [ + ``, + content, + ``, + ].join(''); +} + +/* ============================================================================ + * XML + * ========================================================================== */ + +function parseXml(xml: string): XmlNode { + if ( + typeof xml !== 'string' + ) { + throw new XamlToSvgError( + 'XAML muss ein String sein.', + 'INVALID_INPUT', + ); + } + + if (!xml.trim()) { + throw new XamlToSvgError( + 'Das XAML ist leer.', + 'EMPTY_XAML', + ); + } + + const parser = + new DOMParser(); + + const document = + parser.parseFromString( + xml, + 'application/xml', + ); + + const root = + document.documentElement; + + if (!root) { + throw new XamlToSvgError( + 'Das XAML enthält kein Root-Element.', + 'MISSING_ROOT', + ); + } + + /* + * @xmldom/xmldom erzeugt bei + * ungültigem XML einen parsererror- + * Knoten bzw. kann entsprechende + * Fehlertexte liefern. + */ + const parserError = + findParserError(root); + + if (parserError) { + throw new XamlToSvgError( + `Ungültiges XAML: ${parserError}`, + 'INVALID_XML', + ); + } + + return domToNode(root); +} + +function findParserError( + root: XmlElement, +): string | undefined { + const rootName = + localName(root.nodeName) + .toLowerCase(); + + if ( + rootName === + 'parsererror' + ) { + return ( + getElementText(root) ?? + 'Unbekannter XML-Parserfehler.' + ); + } + + return findParserErrorInChildren( + root, + ); +} + +function findParserErrorInChildren( + element: XmlElement, +): string | undefined { + for ( + let i = 0; + i < element.childNodes.length; + i++ + ) { + const child = + element.childNodes.item(i); + + if ( + !child || + child.nodeType !== 1 + ) { + continue; + } + + const childElement = + child as XmlElement; + + const name = + localName( + childElement.nodeName, + ).toLowerCase(); + + if ( + name === + 'parsererror' + ) { + return ( + getElementText( + childElement, + ) ?? + 'Unbekannter XML-Parserfehler.' + ); + } + + const result = + findParserErrorInChildren( + childElement, + ); + + if (result) { + return result; + } + } + + return undefined; +} + +function getElementText( + element: XmlElement, +): string | undefined { + let result = ''; + + for ( + let i = 0; + i < element.childNodes.length; + i++ + ) { + const child = + element.childNodes.item(i); + + if ( + child && + child.nodeType === 3 && + child.nodeValue + ) { + result += + child.nodeValue; + } + } + + const value = + result.trim(); + + return value || undefined; +} + +function domToNode( + element: XmlElement, +): XmlNode { + const attributes: Record< + string, + string + > = {}; + + for ( + let i = 0; + i < element.attributes.length; + i++ + ) { + const attribute = + element.attributes.item(i); + + if (!attribute) { + continue; + } + + attributes[attribute.name] = + attribute.value; + } + + const children: XmlNode[] = []; + + for ( + let i = 0; + i < element.childNodes.length; + i++ + ) { + const child = + element.childNodes.item(i); + + if ( + child && + child.nodeType === 1 + ) { + children.push( + domToNode( + child as XmlElement, + ), + ); + } + } + + let text: + | string + | undefined; + + for ( + let i = 0; + i < element.childNodes.length; + i++ + ) { + const child = + element.childNodes.item(i); + + if ( + child && + child.nodeType === 3 + ) { + const value = + child.nodeValue?.trim(); + + if (value) { + text = value; + break; + } + } + } + + return { + name: localName( + element.nodeName, + ), + attributes, + children, + text, + }; +} + +/* ============================================================================ + * Rendering + * ========================================================================== */ + +function renderNode( + node: XmlNode, + parentMatrix: Matrix, +): string { + switch (localName(node.name)) { + case 'Canvas': + return renderCanvas( + node, + parentMatrix, + ); + + case 'Path': + return renderPath( + node, + parentMatrix, + ); + + case 'Ellipse': + return renderEllipse( + node, + parentMatrix, + ); + + case 'Rectangle': + return renderRectangle( + node, + parentMatrix, + ); + + case 'Line': + return renderLine( + node, + parentMatrix, + ); + + case 'TextBlock': + return renderTextBlock( + node, + parentMatrix, + ); + + case 'Viewbox': + return node.children + .map((child) => + renderNode( + child, + parentMatrix, + ), + ) + .join(''); + + default: + return node.children + .filter( + (child) => + !isTransformElement( + child, + ), + ) + .map((child) => + renderNode( + child, + parentMatrix, + ), + ) + .join(''); + } +} + +/* ============================================================================ + * Canvas + * ========================================================================== */ + +function renderCanvas( + node: XmlNode, + parentMatrix: Matrix, +): string { + const width = + number(attr(node, 'Width')) ?? 0; + + const height = + number(attr(node, 'Height')) ?? 0; + + let matrix = parentMatrix; + + const left = + number( + attr( + node, + 'Canvas.Left', + ), + ) ?? 0; + + const top = + number( + attr( + node, + 'Canvas.Top', + ), + ) ?? 0; + + if ( + left !== 0 || + top !== 0 + ) { + matrix = multiply( + matrix, + translate(left, top), + ); + } + + matrix = applyRenderTransform( + node, + matrix, + { + x: 0, + y: 0, + width, + height, + }, + ); + + const children = + node.children + .filter( + (child) => + !isTransformElement( + child, + ), + ) + .map((child) => + renderNode( + child, + matrix, + ), + ) + .join(''); + + return [ + ``, + children, + ``, + ].join(''); +} + +/* ============================================================================ + * Path + * ========================================================================== */ + +function renderPath( + node: XmlNode, + parentMatrix: Matrix, +): string { + const data = attr( + node, + 'Data', + ); + + if (!data) { + return ''; + } + + let matrix = parentMatrix; + + const left = + number( + attr( + node, + 'Canvas.Left', + ), + ) ?? 0; + + const top = + number( + attr( + node, + 'Canvas.Top', + ), + ) ?? 0; + + if ( + left !== 0 || + top !== 0 + ) { + matrix = multiply( + matrix, + translate(left, top), + ); + } + + matrix = applyRenderTransform( + node, + matrix, + ); + + const attributes = [ + `d="${escapeXml(data)}"`, + ...styleAttributes(node), + matrixAttribute(matrix), + ].filter(Boolean); + + return ``; +} + +/* ============================================================================ + * Ellipse + * ========================================================================== */ + +function renderEllipse( + node: XmlNode, + parentMatrix: Matrix, +): string { + const left = + number( + attr( + node, + 'Canvas.Left', + ), + ) ?? 0; + + const top = + number( + attr( + node, + 'Canvas.Top', + ), + ) ?? 0; + + const width = + number( + attr(node, 'Width'), + ) ?? 0; + + const height = + number( + attr(node, 'Height'), + ) ?? 0; + + const cx = + left + + width / 2; + + const cy = + top + + height / 2; + + const rx = + width / 2; + + const ry = + height / 2; + + const matrix = + applyRenderTransform( + node, + parentMatrix, + { + x: left, + y: top, + width, + height, + }, + ); + + const attributes = [ + `cx="${format(cx)}"`, + `cy="${format(cy)}"`, + `rx="${format(rx)}"`, + `ry="${format(ry)}"`, + ...styleAttributes(node), + matrixAttribute(matrix), + ].filter(Boolean); + + return ``; +} + +/* ============================================================================ + * Rectangle + * ========================================================================== */ + +function renderRectangle( + node: XmlNode, + parentMatrix: Matrix, +): string { + const x = + number( + attr( + node, + 'Canvas.Left', + ), + ) ?? 0; + + const y = + number( + attr( + node, + 'Canvas.Top', + ), + ) ?? 0; + + const width = + number(attr(node, 'Width')) ?? 0; + + const height = + number(attr(node, 'Height')) ?? 0; + + const matrix = + applyRenderTransform( + node, + parentMatrix, + { + x, + y, + width, + height, + }, + ); + + const attributes = [ + `x="${format(x)}"`, + `y="${format(y)}"`, + `width="${format(width)}"`, + `height="${format(height)}"`, + ...styleAttributes(node), + matrixAttribute(matrix), + ].filter(Boolean); + + return ``; +} + +/* ============================================================================ + * Line + * ========================================================================== */ + +function renderLine( + node: XmlNode, + parentMatrix: Matrix, +): string { + const x1 = + number(attr(node, 'X1')) ?? 0; + + const y1 = + number(attr(node, 'Y1')) ?? 0; + + const x2 = + number(attr(node, 'X2')) ?? 0; + + const y2 = + number(attr(node, 'Y2')) ?? 0; + + let matrix = parentMatrix; + + const left = + number( + attr( + node, + 'Canvas.Left', + ), + ) ?? 0; + + const top = + number( + attr( + node, + 'Canvas.Top', + ), + ) ?? 0; + + if ( + left !== 0 || + top !== 0 + ) { + matrix = multiply( + matrix, + translate(left, top), + ); + } + + matrix = applyRenderTransform( + node, + matrix, + ); + + const attributes = [ + `x1="${format(x1)}"`, + `y1="${format(y1)}"`, + `x2="${format(x2)}"`, + `y2="${format(y2)}"`, + ...styleAttributes(node), + matrixAttribute(matrix), + ].filter(Boolean); + + return ``; +} + +function renderTextBlock( + node: XmlNode, + parentMatrix: Matrix, +): string { + const text = + attr(node, 'Text') ?? + node.text ?? + ''; + + if (!text) { + return ''; + } + + const x = + number( + attr( + node, + 'Canvas.Left', + ), + ) ?? 0; + + const y = + number( + attr( + node, + 'Canvas.Top', + ), + ) ?? 0; + + const fontSize = + number( + attr( + node, + 'FontSize', + ), + ) ?? 12; + + const fontFamily = + attr( + node, + 'FontFamily', + ); + + const fontWeight = + attr( + node, + 'FontWeight', + ); + + const fontStyle = + attr( + node, + 'FontStyle', + ); + + const foreground = + attr( + node, + 'Foreground', + ) ?? + attr( + node, + 'Fill', + ); + + const textAlignment = + attr( + node, + 'TextAlignment', + ); + + let matrix = + parentMatrix; + + matrix = + applyRenderTransform( + node, + matrix, + { + x, + y, + width: 0, + height: fontSize, + }, + ); + + const attributes = [ + `x="${format(x)}"`, + `y="${format( + y + fontSize, + )}"`, + `font-size="${format( + fontSize, + )}"`, + fontFamily + ? `font-family="${escapeXml( + fontFamily, + )}"` + : '', + fontWeight + ? `font-weight="${escapeXml( + fontWeight, + )}"` + : '', + fontStyle + ? `font-style="${escapeXml( + fontStyle, + )}"` + : '', + foreground + ? `fill="${escapeXml( + convertColor( + foreground, + ), + )}"` + : '', + textAlignment + ? `text-anchor="${convertTextAlignment( + textAlignment, + )}"` + : '', + ...textStyleAttributes(node), + matrixAttribute(matrix), + ].filter(Boolean); + + return [ + ``, + escapeXml(text), + ``, + ].join(''); +} +/* ============================================================================ + * Styles + * ========================================================================== */ + +function styleAttributes( + node: XmlNode, +): string[] { + const result: string[] = []; + + const fill = attr( + node, + 'Fill', + ); + + if (fill !== undefined) { + result.push( + `fill="${escapeXml( + convertColor(fill), + )}"`, + ); + } + + const stroke = attr( + node, + 'Stroke', + ); + + if (stroke !== undefined) { + result.push( + `stroke="${escapeXml( + convertColor(stroke), + )}"`, + ); + } + + const strokeThickness = + number( + attr( + node, + 'StrokeThickness', + ), + ); + + if ( + strokeThickness !== undefined + ) { + result.push( + `stroke-width="${format( + strokeThickness, + )}"`, + ); + } + + const opacity = + number( + attr( + node, + 'Opacity', + ), + ); + + if ( + opacity !== undefined + ) { + result.push( + `opacity="${format( + opacity, + )}"`, + ); + } + + const visibility = attr( + node, + 'Visibility', + ); + + if ( + visibility?.toLowerCase() === + 'collapsed' + ) { + result.push( + 'display="none"', + ); + } + + const dash = attr( + node, + 'StrokeDashArray', + ); + + if (dash) { + result.push( + `stroke-dasharray="${escapeXml( + dash.replace(/,/g, ' '), + )}"`, + ); + } + + return result; +} + +function textStyleAttributes( + node: XmlNode, +): string[] { + const result: string[] = []; + + const opacity = + number( + attr( + node, + 'Opacity', + ), + ); + + if ( + opacity !== undefined + ) { + result.push( + `opacity="${format( + opacity, + )}"`, + ); + } + + const visibility = + attr( + node, + 'Visibility', + ); + + if ( + visibility?.toLowerCase() === + 'collapsed' + ) { + result.push( + 'display="none"', + ); + } + + return result; +} +/* ============================================================================ + * Transformation + * ========================================================================== */ + +function applyRenderTransform( + node: XmlNode, + parentMatrix: Matrix, + bounds?: Bounds, +): Matrix { + const transform = + getRenderTransform(node); + + if (!transform) { + return parentMatrix; + } + + const origin = + getRenderTransformOrigin(node); + + const center = + resolveTransformOrigin( + origin, + bounds, + ); + + const transformMatrix = + parseTransform( + transform, + center, + ); + + return multiply( + parentMatrix, + transformMatrix, + ); +} + +function getRenderTransform( + node: XmlNode, +): XmlNode | undefined { + const renderTransform = + node.children.find( + (child) => + localName( + child.name, + ) === + 'RenderTransform', + ); + + if (!renderTransform) { + return undefined; + } + + return renderTransform.children[0]; +} + +/* ============================================================================ + * RenderTransformOrigin + * ========================================================================== */ + +function getRenderTransformOrigin( + node: XmlNode, +): Point { + const originNode = + node.children.find( + (child) => + localName( + child.name, + ) === + 'RenderTransformOrigin', + ); + + if (!originNode?.text) { + return { + x: 0, + y: 0, + }; + } + + const parts = + originNode.text + .split(',') + .map((value) => + Number(value.trim()), + ); + + return { + x: Number.isFinite(parts[0]) + ? parts[0] + : 0, + + y: Number.isFinite(parts[1]) + ? parts[1] + : 0, + }; +} + +function resolveTransformOrigin( + origin: Point, + bounds?: Bounds, +): Point { + if (!bounds) { + return { + x: 0, + y: 0, + }; + } + + return { + x: + bounds.x + + bounds.width * + origin.x, + + y: + bounds.y + + bounds.height * + origin.y, + }; +} + +/* ============================================================================ + * Transform Parser + * ========================================================================== */ + +function parseTransform( + node: XmlNode, + origin: Point, +): Matrix { + switch (localName(node.name)) { + case 'TransformGroup': + return parseTransformGroup( + node, + origin, + ); + + case 'TranslateTransform': + return translate( + number( + attr(node, 'X'), + ) ?? 0, + number( + attr(node, 'Y'), + ) ?? 0, + ); + + case 'RotateTransform': { + const angle = + number( + attr( + node, + 'Angle', + ), + ) ?? 0; + + return aroundPoint( + rotate(angle), + origin, + ); + } + + case 'ScaleTransform': { + const scaleX = + number( + attr( + node, + 'ScaleX', + ), + ) ?? 1; + + const scaleY = + number( + attr( + node, + 'ScaleY', + ), + ) ?? 1; + + return aroundPoint( + scale( + scaleX, + scaleY, + ), + origin, + ); + } + + case 'SkewTransform': { + const angleX = + number( + attr( + node, + 'AngleX', + ), + ) ?? 0; + + const angleY = + number( + attr( + node, + 'AngleY', + ), + ) ?? 0; + + return aroundPoint( + skew( + angleX, + angleY, + ), + origin, + ); + } + + case 'MatrixTransform': + return parseMatrixTransform( + node, + ); + + default: + return identity(); + } +} + +function parseTransformGroup( + node: XmlNode, + origin: Point, +): Matrix { + let result = identity(); + + for ( + const child of node.children + ) { + const childMatrix = + parseTransform( + child, + origin, + ); + + result = multiply( + result, + childMatrix, + ); + } + + return result; +} + +/* ============================================================================ + * MatrixTransform + * ========================================================================== */ + +function parseMatrixTransform( + node: XmlNode, +): Matrix { + const matrixNode = + findFirst( + node, + 'Matrix', + ); + + if (!matrixNode) { + return identity(); + } + + return { + a: + number( + attr( + matrixNode, + 'M11', + ), + ) ?? 1, + + b: + number( + attr( + matrixNode, + 'M12', + ), + ) ?? 0, + + c: + number( + attr( + matrixNode, + 'M21', + ), + ) ?? 0, + + d: + number( + attr( + matrixNode, + 'M22', + ), + ) ?? 1, + + e: + number( + attr( + matrixNode, + 'OffsetX', + ), + ) ?? 0, + + f: + number( + attr( + matrixNode, + 'OffsetY', + ), + ) ?? 0, + }; +} + +/* ============================================================================ + * Matrix + * ========================================================================== */ + +function identity(): Matrix { + return { + a: 1, + b: 0, + c: 0, + d: 1, + e: 0, + f: 0, + }; +} + +function translate( + x: number, + y: number, +): Matrix { + return { + a: 1, + b: 0, + c: 0, + d: 1, + e: x, + f: y, + }; +} + +function scale( + x: number, + y: number, +): Matrix { + return { + a: x, + b: 0, + c: 0, + d: y, + e: 0, + f: 0, + }; +} + +function rotate( + degrees: number, +): Matrix { + const radians = + degrees * + Math.PI / + 180; + + const cos = + Math.cos(radians); + + const sin = + Math.sin(radians); + + return { + a: cos, + b: sin, + c: -sin, + d: cos, + e: 0, + f: 0, + }; +} + +function skew( + angleX: number, + angleY: number, +): Matrix { + return { + a: 1, + b: + Math.tan( + angleY * + Math.PI / + 180, + ), + c: + Math.tan( + angleX * + Math.PI / + 180, + ), + d: 1, + e: 0, + f: 0, + }; +} + +function multiply( + left: Matrix, + right: Matrix, +): Matrix { + return { + a: + left.a * right.a + + left.c * right.b, + + b: + left.b * right.a + + left.d * right.b, + + c: + left.a * right.c + + left.c * right.d, + + d: + left.b * right.c + + left.d * right.d, + + e: + left.a * right.e + + left.c * right.f + + left.e, + + f: + left.b * right.e + + left.d * right.f + + left.f, + }; +} + +function aroundPoint( + matrix: Matrix, + point: Point, +): Matrix { + return multiply( + multiply( + translate( + point.x, + point.y, + ), + matrix, + ), + translate( + -point.x, + -point.y, + ), + ); +} + +/* ============================================================================ + * SVG Matrix + * ========================================================================== */ + +function matrixAttribute( + matrix: Matrix, +): string { + if (isIdentity(matrix)) { + return ''; + } + + return [ + `transform="matrix(`, + format(matrix.a), + ' ', + format(matrix.b), + ' ', + format(matrix.c), + ' ', + format(matrix.d), + ' ', + format(matrix.e), + ' ', + format(matrix.f), + ')"', + ].join(''); +} + +function isIdentity( + matrix: Matrix, +): boolean { + const epsilon = 0.000000001; + + return ( + Math.abs( + matrix.a - 1, + ) < epsilon && + Math.abs( + matrix.b, + ) < epsilon && + Math.abs( + matrix.c, + ) < epsilon && + Math.abs( + matrix.d - 1, + ) < epsilon && + Math.abs( + matrix.e, + ) < epsilon && + Math.abs( + matrix.f, + ) < epsilon + ); +} + +/* ============================================================================ + * Helpers + * ========================================================================== */ + +function attr( + node: XmlNode, + name: string, +): string | undefined { + if ( + node.attributes[name] !== + undefined + ) { + return node.attributes[name]; + } + + const key = + Object.keys( + node.attributes, + ).find( + (key) => + localName(key) === + localName(name), + ); + + return key + ? node.attributes[key] + : undefined; +} + +function number( + value: + | string + | undefined, +): number | undefined { + if ( + value === undefined + ) { + return undefined; + } + + const result = + Number( + value.trim(), + ); + + return Number.isFinite(result) + ? result + : undefined; +} + +function localName( + value: string, +): string { + const index = + value.indexOf(':'); + + if (index < 0) { + return value; + } + + return value.substring( + index + 1, + ); +} + +function findFirst( + node: XmlNode, + name: string, +): XmlNode | undefined { + if ( + localName(node.name) === + name + ) { + return node; + } + + for ( + const child of node.children + ) { + const result = + findFirst( + child, + name, + ); + + if (result) { + return result; + } + } + + return undefined; +} + +function isTransformElement( + node: XmlNode, +): boolean { + const name = + localName(node.name); + + return ( + name === + 'RenderTransform' || + name === + 'RenderTransformOrigin' + ); +} + +function format( + value: number, +): string { + if ( + Math.abs(value) < + 0.000000001 + ) { + return '0'; + } + + return Number( + value.toFixed(8), + ).toString(); +} + +function escapeXml( + value: string, +): string { + return value + .replace( + /&/g, + '&', + ) + .replace( + /"/g, + '"', + ) + .replace( + //g, + '>', + ); +} + +function convertColor( + value: string, +): string { + if ( + value.toLowerCase() === + 'transparent' + ) { + return 'none'; + } + + return value; +} + +function convertTextAlignment( + value: string, +): string { + switch ( + value.toLowerCase() + ) { + case 'center': + return 'middle'; + + case 'right': + return 'end'; + + case 'left': + default: + return 'start'; + } +} + +function indentXml( + value: string, +): string { + return value + .replace( + /(]*>|<\/g>|]*\/>|]*\/>|]*\/>|]*\/>)/g, + '\n $1', + ) + .trim(); +} \ No newline at end of file diff --git a/src/components/procon/svg/xaml_to_svg.ts b/src/components/procon/svg/xaml_to_svg.ts new file mode 100644 index 0000000..02f8ef6 --- /dev/null +++ b/src/components/procon/svg/xaml_to_svg.ts @@ -0,0 +1,2484 @@ +import { + DOMParser, + type Element as XmlElement, +} from '@xmldom/xmldom'; + +/* ============================================================================ + * Public API + * ========================================================================== */ + +export interface XamlToSvgOptions { + pretty?: boolean; + width?: number | string; + height?: number | string; + viewBox?: string; +} + +export class XamlToSvgError extends Error { + public readonly code: string; + + constructor( + message: string, + code: string = 'XAML_ERROR', + ) { + super(message); + + this.name = 'XamlToSvgError'; + this.code = code; + + Object.setPrototypeOf( + this, + XamlToSvgError.prototype, + ); + } +} + +/* ============================================================================ + * Internal types + * ========================================================================== */ + +interface XmlNode { + name: string; + attributes: Record; + children: XmlNode[]; + text?: string; +} + +interface Matrix { + a: number; + b: number; + c: number; + d: number; + e: number; + f: number; +} + +interface Point { + x: number; + y: number; +} + +interface Bounds { + x: number; + y: number; + width: number; + height: number; +} + +/* ============================================================================ + * Main + * ========================================================================== */ + +export function xamlToSvg( + xaml: string, + options: XamlToSvgOptions = {}, +): string { + if ( + typeof xaml !== 'string' || + xaml.trim() === '' + ) { + throw new XamlToSvgError( + 'Das XAML ist leer.', + 'EMPTY_XAML', + ); + } + + const root = parseXml(xaml); + + /* + * Viewbox ist in XAML kein sichtbares Element. + * Wir suchen deshalb Canvas rekursiv. + */ + const canvas = + findFirst(root, 'Canvas'); + + if (!canvas) { + throw new XamlToSvgError( + 'Kein Canvas im XAML gefunden.', + 'NO_CANVAS', + ); + } + + /* + * Die oberste Canvas-Größe wird bevorzugt verwendet. + */ + const explicitWidth = + number( + attr(canvas, 'Width'), + ); + + const explicitHeight = + number( + attr(canvas, 'Height'), + ); + + const inferredBounds = + calculateNodeBounds(canvas); + + const canvasWidth = + explicitWidth ?? + inferredBounds.width ?? + 100; + + const canvasHeight = + explicitHeight ?? + inferredBounds.height ?? + 100; + + if ( + !Number.isFinite(canvasWidth) || + !Number.isFinite(canvasHeight) || + canvasWidth <= 0 || + canvasHeight <= 0 + ) { + throw new XamlToSvgError( + `Ungültige Canvas-Größe: ${canvasWidth} x ${canvasHeight}.`, + 'INVALID_CANVAS_SIZE', + ); + } + + const width = + options.width ?? + canvasWidth; + + const height = + options.height ?? + canvasHeight; + + const viewBox = + options.viewBox ?? + `0 0 ${format(canvasWidth)} ${format(canvasHeight)}`; + + if (!isValidViewBox(viewBox)) { + throw new XamlToSvgError( + `Ungültige viewBox: "${viewBox}".`, + 'INVALID_VIEWBOX', + ); + } + + const content = + renderNode( + canvas, + identity(), + ); + + if (!content.trim()) { + throw new XamlToSvgError( + 'Das XAML enthält keine darstellbaren Elemente.', + 'EMPTY_CONTENT', + ); + } + + const svgStart = + ``; + + if (options.pretty) { + return [ + svgStart, + indentXml(content), + '', + ].join('\n'); + } + + return ( + svgStart + + content + + '' + ); +} + +/* ============================================================================ + * XML + * ========================================================================== */ + +function parseXml( + xml: string, +): XmlNode { + let document: any; + + try { + const parser = + new DOMParser(); + + /* + * Kein errorHandler hier. + * + * Das vermeidet Versionsprobleme mit + * @xmldom/xmldom und dessen TypeScript-Typen. + */ + document = + parser.parseFromString( + xml, + 'application/xml', + ); + } catch (error) { + throw new XamlToSvgError( + `XML konnte nicht geparst werden: ${getErrorMessage(error)}`, + 'XML_PARSE_ERROR', + ); + } + + if (!document) { + throw new XamlToSvgError( + 'Der XML-Parser hat kein Dokument zurückgegeben.', + 'XML_PARSE_ERROR', + ); + } + + const root = + document.documentElement; + + if (!root) { + throw new XamlToSvgError( + 'XAML enthält kein Root-Element.', + 'NO_ROOT', + ); + } + + /* + * @xmldom kann bei manchen Fehlern trotzdem ein Dokument liefern. + */ + const parserError = + findParserError(document); + + if (parserError) { + throw new XamlToSvgError( + `Ungültiges XML/XAML: ${parserError}`, + 'INVALID_XML', + ); + } + + return domToNode( + root as XmlElement, + ); +} + +function findParserError( + document: any, +): string | undefined { + try { + const errors = + document.getElementsByTagName( + 'parsererror', + ); + + if ( + errors && + errors.length > 0 + ) { + const error = + errors.item(0); + + return ( + error?.textContent || + 'Unbekannter XML-Fehler.' + ); + } + } catch { + // Ignore + } + + return undefined; +} + +function domToNode( + element: XmlElement, +): XmlNode { + const attributes: Record< + string, + string + > = {}; + + for ( + let i = 0; + i < element.attributes.length; + i++ + ) { + const attribute = + element.attributes.item(i); + + if (!attribute) { + continue; + } + + attributes[ + attribute.name + ] = attribute.value; + } + + const children: XmlNode[] = []; + + for ( + let i = 0; + i < element.childNodes.length; + i++ + ) { + const child = + element.childNodes.item(i); + + if ( + child && + child.nodeType === 1 + ) { + children.push( + domToNode( + child as XmlElement, + ), + ); + } + } + + let text: + | string + | undefined; + + for ( + let i = 0; + i < element.childNodes.length; + i++ + ) { + const child = + element.childNodes.item(i); + + if ( + child && + child.nodeType === 3 + ) { + const value = + child.nodeValue?.trim(); + + if (value) { + text = value; + break; + } + } + } + + return { + name: element.nodeName, + attributes, + children, + text, + }; +} + +/* ============================================================================ + * Rendering + * ========================================================================== */ + +function renderNode( + node: XmlNode, + parentMatrix: Matrix, +): string { + const name = + localName(node.name); + + switch (name) { + case 'Canvas': + return renderCanvas( + node, + parentMatrix, + ); + + case 'Path': + return renderPath( + node, + parentMatrix, + ); + + case 'Ellipse': + return renderEllipse( + node, + parentMatrix, + ); + + case 'Rectangle': + return renderRectangle( + node, + parentMatrix, + ); + + case 'Line': + return renderLine( + node, + parentMatrix, + ); + + case 'TextBlock': + return renderTextBlock( + node, + parentMatrix, + ); + + case 'Viewbox': + return node.children + .map((child) => + renderNode( + child, + parentMatrix, + ), + ) + .join(''); + + default: + /* + * Unbekannte XAML-Elemente werden nicht sofort + * verworfen. Ihre darstellbaren Kinder werden + * weiter verarbeitet. + */ + return node.children + .filter( + (child) => + !isPropertyElement( + child, + ), + ) + .map((child) => + renderNode( + child, + parentMatrix, + ), + ) + .join(''); + } +} + +/* ============================================================================ + * Canvas + * ========================================================================== */ + +function renderCanvas( + node: XmlNode, + parentMatrix: Matrix, +): string { + /* + * Canvas.Left / Canvas.Top + */ + const left = + number( + attr( + node, + 'Canvas.Left', + ), + ) ?? 0; + + const top = + number( + attr( + node, + 'Canvas.Top', + ), + ) ?? 0; + + let matrix = + parentMatrix; + + if ( + left !== 0 || + top !== 0 + ) { + matrix = + multiply( + matrix, + translate( + left, + top, + ), + ); + } + + /* + * Bounds des Canvas. + * + * Wenn Width/Height vorhanden sind, + * verwenden wir diese. + * + * Wenn nicht, werden die Bounds aus + * den Kindern berechnet. + */ + const explicitWidth = + number( + attr( + node, + 'Width', + ), + ); + + const explicitHeight = + number( + attr( + node, + 'Height', + ), + ); + + const inferred = + calculateNodeBounds(node); + + const bounds: Bounds = { + x: + inferred.x, + + y: + inferred.y, + + width: + explicitWidth ?? + inferred.width, + + height: + explicitHeight ?? + inferred.height, + }; + + /* + * RenderTransform + */ + matrix = + applyRenderTransform( + node, + matrix, + bounds, + ); + + const children = + node.children + .filter( + (child) => + !isPropertyElement( + child, + ), + ) + .map((child) => + renderNode( + child, + matrix, + ), + ) + .join(''); + + return ( + `` + + children + + '' + ); +} + +/* ============================================================================ + * Path + * ========================================================================== */ + +function renderPath( + node: XmlNode, + parentMatrix: Matrix, +): string { + const data = + attr( + node, + 'Data', + ); + + if (!data) { + return ''; + } + + let matrix = + parentMatrix; + + const left = + number( + attr( + node, + 'Canvas.Left', + ), + ) ?? 0; + + const top = + number( + attr( + node, + 'Canvas.Top', + ), + ) ?? 0; + + if ( + left !== 0 || + top !== 0 + ) { + matrix = + multiply( + matrix, + translate( + left, + top, + ), + ); + } + + const bounds = + pathBounds(data); + + matrix = + applyRenderTransform( + node, + matrix, + bounds, + ); + + const attributes = [ + `d="${escapeXml(data)}"`, + ...styleAttributes(node), + matrixAttribute(matrix), + ].filter(Boolean); + + return ( + `` + ); +} + +/* ============================================================================ + * Ellipse + * ========================================================================== */ + +function renderEllipse( + node: XmlNode, + parentMatrix: Matrix, +): string { + const left = + number( + attr( + node, + 'Canvas.Left', + ), + ) ?? 0; + + const top = + number( + attr( + node, + 'Canvas.Top', + ), + ) ?? 0; + + const width = + number( + attr( + node, + 'Width', + ), + ) ?? 0; + + const height = + number( + attr( + node, + 'Height', + ), + ) ?? 0; + + const cx = + left + + width / 2; + + const cy = + top + + height / 2; + + const rx = + width / 2; + + const ry = + height / 2; + + const matrix = + applyRenderTransform( + node, + parentMatrix, + { + x: left, + y: top, + width, + height, + }, + ); + + const attributes = [ + `cx="${format(cx)}"`, + `cy="${format(cy)}"`, + `rx="${format(rx)}"`, + `ry="${format(ry)}"`, + ...styleAttributes(node), + matrixAttribute(matrix), + ].filter(Boolean); + + return ( + `` + ); +} + +/* ============================================================================ + * Rectangle + * ========================================================================== */ + +function renderRectangle( + node: XmlNode, + parentMatrix: Matrix, +): string { + const x = + number( + attr( + node, + 'Canvas.Left', + ), + ) ?? 0; + + const y = + number( + attr( + node, + 'Canvas.Top', + ), + ) ?? 0; + + const width = + number( + attr( + node, + 'Width', + ), + ) ?? 0; + + const height = + number( + attr( + node, + 'Height', + ), + ) ?? 0; + + const matrix = + applyRenderTransform( + node, + parentMatrix, + { + x, + y, + width, + height, + }, + ); + + const attributes = [ + `x="${format(x)}"`, + `y="${format(y)}"`, + `width="${format(width)}"`, + `height="${format(height)}"`, + ...styleAttributes(node), + matrixAttribute(matrix), + ].filter(Boolean); + + return ( + `` + ); +} + +/* ============================================================================ + * Line + * ========================================================================== */ + +function renderLine( + node: XmlNode, + parentMatrix: Matrix, +): string { + const x1 = + number( + attr(node, 'X1'), + ) ?? 0; + + const y1 = + number( + attr(node, 'Y1'), + ) ?? 0; + + const x2 = + number( + attr(node, 'X2'), + ) ?? 0; + + const y2 = + number( + attr(node, 'Y2'), + ) ?? 0; + + let matrix = + parentMatrix; + + const left = + number( + attr( + node, + 'Canvas.Left', + ), + ) ?? 0; + + const top = + number( + attr( + node, + 'Canvas.Top', + ), + ) ?? 0; + + if ( + left !== 0 || + top !== 0 + ) { + matrix = + multiply( + matrix, + translate( + left, + top, + ), + ); + } + + matrix = + applyRenderTransform( + node, + matrix, + ); + + const attributes = [ + `x1="${format(x1)}"`, + `y1="${format(y1)}"`, + `x2="${format(x2)}"`, + `y2="${format(y2)}"`, + ...styleAttributes(node), + matrixAttribute(matrix), + ].filter(Boolean); + + return ( + `` + ); +} + +/* ============================================================================ + * TextBlock + * ========================================================================== */ + +function renderTextBlock( + node: XmlNode, + parentMatrix: Matrix, +): string { + const text = + node.text ?? ''; + + if (!text) { + return ''; + } + + const x = + number( + attr( + node, + 'Canvas.Left', + ), + ) ?? 0; + + const y = + number( + attr( + node, + 'Canvas.Top', + ), + ) ?? 0; + + const fontSize = + number( + attr( + node, + 'FontSize', + ), + ) ?? 12; + + const fontFamily = + attr( + node, + 'FontFamily', + ); + + const fontWeight = + attr( + node, + 'FontWeight', + ); + + const fontStyle = + attr( + node, + 'FontStyle', + ); + + const bounds: Bounds = { + x, + y, + width: + number( + attr( + node, + 'Width', + ), + ) ?? 0, + height: + number( + attr( + node, + 'Height', + ), + ) ?? fontSize, + }; + + const matrix = + applyRenderTransform( + node, + parentMatrix, + bounds, + ); + + /* + * SVG Text verwendet die Baseline. + * Canvas.Top bei WPF bezieht sich eher auf + * die obere Textkante. + */ + const baselineY = + y + fontSize; + + const attributes = [ + `x="${format(x)}"`, + `y="${format(baselineY)}"`, + `font-size="${format(fontSize)}"`, + + fontFamily + ? `font-family="${escapeXml(fontFamily)}"` + : '', + + fontWeight + ? `font-weight="${escapeXml(fontWeight)}"` + : '', + + fontStyle + ? `font-style="${escapeXml(fontStyle)}"` + : '', + + ...styleAttributes(node), + matrixAttribute(matrix), + ].filter(Boolean); + + return ( + `${escapeXml(text)}` + ); +} + +/* ============================================================================ + * Styles + * ========================================================================== */ + +function styleAttributes( + node: XmlNode, +): string[] { + const result: string[] = []; + + const fill = + attr(node, 'Fill'); + + if ( + fill !== undefined + ) { + result.push( + `fill="${escapeXml( + convertColor(fill), + )}"`, + ); + } + + const stroke = + attr(node, 'Stroke'); + + if ( + stroke !== undefined + ) { + result.push( + `stroke="${escapeXml( + convertColor(stroke), + )}"`, + ); + } + + const strokeThickness = + number( + attr( + node, + 'StrokeThickness', + ), + ); + + if ( + strokeThickness !== + undefined + ) { + result.push( + `stroke-width="${format( + strokeThickness, + )}"`, + ); + } + + const opacity = + number( + attr( + node, + 'Opacity', + ), + ); + + if ( + opacity !== undefined + ) { + result.push( + `opacity="${format( + opacity, + )}"`, + ); + } + + const visibility = + attr( + node, + 'Visibility', + ); + + if ( + visibility?.toLowerCase() === + 'collapsed' + ) { + result.push( + 'display="none"', + ); + } + + const dash = + attr( + node, + 'StrokeDashArray', + ); + + if (dash) { + result.push( + `stroke-dasharray="${escapeXml( + dash.replace( + /,/g, + ' ', + ), + )}"`, + ); + } + + return result; +} + +/* ============================================================================ + * Transform + * ========================================================================== */ + +function applyRenderTransform( + node: XmlNode, + parentMatrix: Matrix, + bounds?: Bounds, +): Matrix { + const transform = + getRenderTransform(node); + + if (!transform) { + return parentMatrix; + } + + const origin = + getRenderTransformOrigin( + node, + ); + + const point = + resolveTransformOrigin( + origin, + bounds, + ); + + const transformMatrix = + parseTransform( + transform, + point, + ); + + return multiply( + parentMatrix, + transformMatrix, + ); +} + +function getRenderTransform( + node: XmlNode, +): XmlNode | undefined { + const property = + node.children.find( + (child) => + localName( + child.name, + ) === + 'RenderTransform', + ); + + if (!property) { + return undefined; + } + + if ( + property.children.length === + 0 + ) { + throw new XamlToSvgError( + 'RenderTransform ist leer.', + 'INVALID_RENDER_TRANSFORM', + ); + } + + /* + * Beispiel: + * + * + * + * ... + * + * + */ + return property.children[0]; +} + +function getRenderTransformOrigin( + node: XmlNode, +): Point { + const property = + node.children.find( + (child) => + localName( + child.name, + ) === + 'RenderTransformOrigin', + ); + + if (!property) { + return { + x: 0, + y: 0, + }; + } + + if (!property.text) { + throw new XamlToSvgError( + 'RenderTransformOrigin ist leer.', + 'INVALID_RENDER_ORIGIN', + ); + } + + const parts = + property.text + .trim() + .split(',') + .map( + (value) => + Number( + value.trim(), + ), + ); + + if ( + parts.length !== 2 || + !Number.isFinite( + parts[0], + ) || + !Number.isFinite( + parts[1], + ) + ) { + throw new XamlToSvgError( + `Ungültiger RenderTransformOrigin: "${property.text}". Erwartet wird z.B. "0.5,0.5".`, + 'INVALID_RENDER_ORIGIN', + ); + } + + return { + x: parts[0], + y: parts[1], + }; +} + +function resolveTransformOrigin( + origin: Point, + bounds?: Bounds, +): Point { + if (!bounds) { + return { + x: 0, + y: 0, + }; + } + + return { + x: + bounds.x + + bounds.width * + origin.x, + + y: + bounds.y + + bounds.height * + origin.y, + }; +} + +/* ============================================================================ + * Transform parser + * ========================================================================== */ + +function parseTransform( + node: XmlNode, + origin: Point, +): Matrix { + const name = + localName(node.name); + + switch (name) { + case 'TransformGroup': + return parseTransformGroup( + node, + origin, + ); + + case 'TranslateTransform': + return translate( + number( + attr( + node, + 'X', + ), + ) ?? 0, + + number( + attr( + node, + 'Y', + ), + ) ?? 0, + ); + + case 'RotateTransform': { + const angle = + number( + attr( + node, + 'Angle', + ), + ) ?? 0; + + return aroundPoint( + rotate(angle), + origin, + ); + } + + case 'ScaleTransform': { + const scaleX = + number( + attr( + node, + 'ScaleX', + ), + ) ?? 1; + + const scaleY = + number( + attr( + node, + 'ScaleY', + ), + ) ?? 1; + + return aroundPoint( + scale( + scaleX, + scaleY, + ), + origin, + ); + } + + case 'SkewTransform': { + const angleX = + number( + attr( + node, + 'AngleX', + ), + ) ?? 0; + + const angleY = + number( + attr( + node, + 'AngleY', + ), + ) ?? 0; + + return aroundPoint( + skew( + angleX, + angleY, + ), + origin, + ); + } + + case 'MatrixTransform': + return parseMatrixTransform( + node, + ); + + default: + return identity(); + } +} + +function parseTransformGroup( + node: XmlNode, + origin: Point, +): Matrix { + let result = + identity(); + + /* + * XAML: + * + * + * + * + * + * + * Die Reihenfolge ist wichtig. + * + * Wir multiplizieren die Matrizen + * in der Reihenfolge, in der sie + * im XAML vorkommen. + */ + for ( + const child of node.children + ) { + if ( + isPropertyElement(child) + ) { + continue; + } + + const childMatrix = + parseTransform( + child, + origin, + ); + + result = + multiply( + result, + childMatrix, + ); + } + + return result; +} + +/* ============================================================================ + * MatrixTransform + * ========================================================================== */ + +function parseMatrixTransform( + node: XmlNode, +): Matrix { + const matrixNode = + findFirst( + node, + 'Matrix', + ); + + if (!matrixNode) { + throw new XamlToSvgError( + 'MatrixTransform enthält kein Matrix-Element.', + 'INVALID_MATRIX_TRANSFORM', + ); + } + + return { + a: + number( + attr( + matrixNode, + 'M11', + ), + ) ?? 1, + + b: + number( + attr( + matrixNode, + 'M12', + ), + ) ?? 0, + + c: + number( + attr( + matrixNode, + 'M21', + ), + ) ?? 0, + + d: + number( + attr( + matrixNode, + 'M22', + ), + ) ?? 1, + + e: + number( + attr( + matrixNode, + 'OffsetX', + ), + ) ?? 0, + + f: + number( + attr( + matrixNode, + 'OffsetY', + ), + ) ?? 0, + }; +} + +/* ============================================================================ + * Matrix + * ========================================================================== */ + +function identity(): Matrix { + return { + a: 1, + b: 0, + c: 0, + d: 1, + e: 0, + f: 0, + }; +} + +function translate( + x: number, + y: number, +): Matrix { + return { + a: 1, + b: 0, + c: 0, + d: 1, + e: x, + f: y, + }; +} + +function scale( + x: number, + y: number, +): Matrix { + return { + a: x, + b: 0, + c: 0, + d: y, + e: 0, + f: 0, + }; +} + +function rotate( + degrees: number, +): Matrix { + const radians = + degrees * + Math.PI / + 180; + + const cos = + Math.cos(radians); + + const sin = + Math.sin(radians); + + return { + a: cos, + b: sin, + c: -sin, + d: cos, + e: 0, + f: 0, + }; +} + +function skew( + angleX: number, + angleY: number, +): Matrix { + return { + a: 1, + + b: + Math.tan( + angleY * + Math.PI / + 180, + ), + + c: + Math.tan( + angleX * + Math.PI / + 180, + ), + + d: 1, + e: 0, + f: 0, + }; +} + +function multiply( + left: Matrix, + right: Matrix, +): Matrix { + return { + a: + left.a * right.a + + left.c * right.b, + + b: + left.b * right.a + + left.d * right.b, + + c: + left.a * right.c + + left.c * right.d, + + d: + left.b * right.c + + left.d * right.d, + + e: + left.a * right.e + + left.c * right.f + + left.e, + + f: + left.b * right.e + + left.d * right.f + + left.f, + }; +} + +function aroundPoint( + matrix: Matrix, + point: Point, +): Matrix { + return multiply( + multiply( + translate( + point.x, + point.y, + ), + matrix, + ), + translate( + -point.x, + -point.y, + ), + ); +} + +/* ============================================================================ + * SVG matrix + * ========================================================================== */ + +function matrixAttribute( + matrix: Matrix, +): string { + if ( + isIdentity(matrix) + ) { + return ''; + } + + return ( + ` transform="matrix(` + + `${format(matrix.a)} ` + + `${format(matrix.b)} ` + + `${format(matrix.c)} ` + + `${format(matrix.d)} ` + + `${format(matrix.e)} ` + + `${format(matrix.f)}` + + `)"` + ); +} + +function isIdentity( + matrix: Matrix, +): boolean { + const epsilon = + 0.000000001; + + return ( + Math.abs( + matrix.a - 1, + ) < epsilon && + Math.abs( + matrix.b, + ) < epsilon && + Math.abs( + matrix.c, + ) < epsilon && + Math.abs( + matrix.d - 1, + ) < epsilon && + Math.abs( + matrix.e, + ) < epsilon && + Math.abs( + matrix.f, + ) < epsilon + ); +} + +/* ============================================================================ + * Bounds + * ========================================================================== */ + +function calculateNodeBounds( + node: XmlNode, +): Bounds { + /* + * Hat das Element selbst Width/Height, + * werden diese bevorzugt. + */ + const explicitWidth = + number( + attr( + node, + 'Width', + ), + ); + + const explicitHeight = + number( + attr( + node, + 'Height', + ), + ); + + if ( + explicitWidth !== undefined || + explicitHeight !== undefined + ) { + const inferred = + calculateChildrenBounds( + node, + ); + + return { + x: inferred.x, + y: inferred.y, + + width: + explicitWidth ?? + inferred.width, + + height: + explicitHeight ?? + inferred.height, + }; + } + + return calculateChildrenBounds( + node, + ); +} + +function calculateChildrenBounds( + node: XmlNode, +): Bounds { + let result: + | Bounds + | undefined; + + for ( + const child of node.children + ) { + if ( + isPropertyElement(child) + ) { + continue; + } + + const bounds = + getElementBounds( + child, + ); + + if (!bounds) { + continue; + } + + result = + result + ? unionBounds( + result, + bounds, + ) + : bounds; + } + + return ( + result ?? { + x: 0, + y: 0, + width: 0, + height: 0, + } + ); +} + +function getElementBounds( + node: XmlNode, +): Bounds | undefined { + const name = + localName(node.name); + + switch (name) { + case 'Canvas': + return calculateNodeBounds( + node, + ); + + case 'Rectangle': { + const x = + number( + attr( + node, + 'Canvas.Left', + ), + ) ?? 0; + + const y = + number( + attr( + node, + 'Canvas.Top', + ), + ) ?? 0; + + const width = + number( + attr( + node, + 'Width', + ), + ) ?? 0; + + const height = + number( + attr( + node, + 'Height', + ), + ) ?? 0; + + return { + x, + y, + width, + height, + }; + } + + case 'Ellipse': { + const x = + number( + attr( + node, + 'Canvas.Left', + ), + ) ?? 0; + + const y = + number( + attr( + node, + 'Canvas.Top', + ), + ) ?? 0; + + const width = + number( + attr( + node, + 'Width', + ), + ) ?? 0; + + const height = + number( + attr( + node, + 'Height', + ), + ) ?? 0; + + return { + x, + y, + width, + height, + }; + } + + case 'Line': { + const x1 = + number( + attr( + node, + 'X1', + ), + ) ?? 0; + + const y1 = + number( + attr( + node, + 'Y1', + ), + ) ?? 0; + + const x2 = + number( + attr( + node, + 'X2', + ), + ) ?? 0; + + const y2 = + number( + attr( + node, + 'Y2', + ), + ) ?? 0; + + return { + x: Math.min( + x1, + x2, + ), + + y: Math.min( + y1, + y2, + ), + + width: + Math.abs( + x2 - x1, + ), + + height: + Math.abs( + y2 - y1, + ), + }; + } + + case 'Path': { + const data = + attr( + node, + 'Data', + ); + + if (!data) { + return undefined; + } + + return pathBounds( + data, + ); + } + + case 'TextBlock': { + const x = + number( + attr( + node, + 'Canvas.Left', + ), + ) ?? 0; + + const y = + number( + attr( + node, + 'Canvas.Top', + ), + ) ?? 0; + + const width = + number( + attr( + node, + 'Width', + ), + ) ?? 0; + + const height = + number( + attr( + node, + 'Height', + ), + ) ?? + number( + attr( + node, + 'FontSize', + ), + ) ?? + 12; + + return { + x, + y, + width, + height, + }; + } + + default: + return undefined; + } +} + +function unionBounds( + a: Bounds, + b: Bounds, +): Bounds { + const x = + Math.min( + a.x, + b.x, + ); + + const y = + Math.min( + a.y, + b.y, + ); + + const right = + Math.max( + a.x + a.width, + b.x + b.width, + ); + + const bottom = + Math.max( + a.y + a.height, + b.y + b.height, + ); + + return { + x, + y, + width: + right - x, + height: + bottom - y, + }; +} + +/* ============================================================================ + * Path bounds + * + * Für TransformOrigin benötigen wir eine Bounding Box. + * + * Der Parser ist bewusst einfach gehalten und berücksichtigt + * die Zahlenwerte des Path. Für typische XAML-Paths wie: + * + * M 8,0 H 280 A 8,8 ... H 8 A 8,8 ... Z + * + * funktioniert das für die Bestimmung des Rotationszentrums. + * ========================================================================== */ + +function pathBounds( + data: string, +): Bounds { + /* + * Wir entfernen zunächst die Befehle und extrahieren + * alle Zahlen. + */ + const matches = + data.match( + /[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g, + ); + + if (!matches) { + return { + x: 0, + y: 0, + width: 0, + height: 0, + }; + } + + const values = + matches.map(Number); + + let minX = + Number.POSITIVE_INFINITY; + + let minY = + Number.POSITIVE_INFINITY; + + let maxX = + Number.NEGATIVE_INFINITY; + + let maxY = + Number.NEGATIVE_INFINITY; + + /* + * Für einfache XAML-Paths reicht die Ermittlung + * aus den Koordinaten meistens aus. + * + * Bei Arc-Radien können dadurch die Bounds etwas + * größer/kleiner ausfallen. Für den TransformOrigin + * des Beispiels ist das ausreichend. + */ + for ( + let i = 0; + i < values.length; + i++ + ) { + if ( + !Number.isFinite( + values[i], + ) + ) { + continue; + } + + /* + * Wir verwenden Zahlen paarweise als X/Y. + */ + if ( + i + 1 < + values.length + ) { + const x = + values[i]; + + const y = + values[i + 1]; + + if ( + Number.isFinite(x) && + Number.isFinite(y) + ) { + minX = + Math.min( + minX, + x, + ); + + minY = + Math.min( + minY, + y, + ); + + maxX = + Math.max( + maxX, + x, + ); + + maxY = + Math.max( + maxY, + y, + ); + } + } + } + + if ( + !Number.isFinite( + minX, + ) || + !Number.isFinite( + minY, + ) || + !Number.isFinite( + maxX, + ) || + !Number.isFinite( + maxY, + ) + ) { + return { + x: 0, + y: 0, + width: 0, + height: 0, + }; + } + + return { + x: minX, + y: minY, + width: + maxX - minX, + height: + maxY - minY, + }; +} + +/* ============================================================================ + * Helpers + * ========================================================================== */ + +function attr( + node: XmlNode, + name: string, +): string | undefined { + if ( + node.attributes[name] !== + undefined + ) { + return node.attributes[name]; + } + + const wanted = + localName(name); + + const key = + Object.keys( + node.attributes, + ).find( + (key) => + localName(key) === + wanted, + ); + + return key + ? node.attributes[key] + : undefined; +} + +function number( + value: + | string + | undefined, +): number | undefined { + if ( + value === undefined + ) { + return undefined; + } + + const result = + Number( + value.trim(), + ); + + return Number.isFinite(result) + ? result + : undefined; +} + +/* + * Wichtig: + * + * x:Canvas + * Canvas.RenderTransform + * Canvas.RenderTransformOrigin + * + * werden alle korrekt auf ihren eigentlichen lokalen Namen reduziert. + */ +function localName( + value: string, +): string { + let result = + value.trim(); + + /* + * XML Namespace + * + * p:Canvas + * -> + * Canvas + */ + const colonIndex = + result.lastIndexOf(':'); + + if (colonIndex >= 0) { + result = + result.substring( + colonIndex + 1, + ); + } + + /* + * XAML Property Element + * + * Canvas.RenderTransform + * -> + * RenderTransform + */ + const dotIndex = + result.lastIndexOf('.'); + + if (dotIndex >= 0) { + result = + result.substring( + dotIndex + 1, + ); + } + + return result; +} + +function findFirst( + node: XmlNode, + name: string, +): XmlNode | undefined { + if ( + localName(node.name) === + name + ) { + return node; + } + + for ( + const child of node.children + ) { + const result = + findFirst( + child, + name, + ); + + if (result) { + return result; + } + } + + return undefined; +} + +function isPropertyElement( + node: XmlNode, +): boolean { + const name = + node.name; + + /* + * Canvas.RenderTransform + * Canvas.RenderTransformOrigin + * + * Stroke... + * + * Alles, was explizit als XAML + * Property Element geschrieben wurde. + */ + return ( + name.includes('.') + ); +} + +function format( + value: number, +): string { + if ( + Math.abs(value) < + 0.000000001 + ) { + return '0'; + } + + return Number( + value.toFixed(8), + ).toString(); +} + +function escapeXml( + value: string, +): string { + return value + .replace( + /&/g, + '&', + ) + .replace( + /"/g, + '"', + ) + .replace( + //g, + '>', + ); +} + +function convertColor( + value: string, +): string { + const color = + value + .trim() + .toLowerCase(); + + if ( + color === + 'transparent' + ) { + return 'none'; + } + + /* + * WPF verwendet manchmal #AARRGGBB. + * + * SVG erwartet bei Hex-Farben normalerweise: + * #RRGGBB + * + * Wir wandeln #AARRGGBB in rgb/rgba um. + */ + if ( + /^#[0-9a-f]{8}$/i.test( + color, + ) + ) { + const a = + parseInt( + color.substring( + 1, + 3, + ), + 16, + ) / 255; + + const r = + color.substring( + 3, + 5, + ); + + const g = + color.substring( + 5, + 7, + ); + + const b = + color.substring( + 7, + 9, + ); + + return ( + `rgba(` + + `${parseInt(r, 16)},` + + `${parseInt(g, 16)},` + + `${parseInt(b, 16)},` + + `${format(a)}` + + `)` + ); + } + + return value; +} + +function indentXml( + value: string, +): string { + return value + .replace( + /(]*>|<\/g>|]*\/>|]*\/>|]*\/>|]*\/>|]*>.*?<\/text>)/g, + '\n $1', + ) + .trim(); +} + +function isValidViewBox( + value: string, +): boolean { + const parts = + value + .trim() + .split( + /[\s,]+/, + ) + .map(Number); + + return ( + parts.length === 4 && + parts.every( + (value) => + Number.isFinite( + value, + ), + ) + ); +} + +function getErrorMessage( + error: unknown, +): string { + if ( + error instanceof Error + ) { + return error.message; + } + + return String(error); +} \ No newline at end of file diff --git a/src/components/procon/visualiserungPositionen.tsx b/src/components/procon/visualiserungPositionen.tsx new file mode 100644 index 0000000..ddbce50 --- /dev/null +++ b/src/components/procon/visualiserungPositionen.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Modal from "../ui/Modal"; +import { InputNumber } from "../ui/InputNumber"; +import { api_ProconVisualisierungen_Update } from "@/actions/procon/api-procon-visualisierungen"; +import { ProConView } from "@/types/procon"; + +interface Positionen{ + posX:number, + posY:number, + height:number, + width:number, + zIndex:number +} + +interface Props { + id:number, + koordinaten: Positionen +} + + +export default function VisualisierungPositionen({ id,koordinaten }: Props) { + const [showDetails, setShowDetails] = useState(false); + const [text, setText] = useState(''); + + const handleChange = (name:string,value:number)=>{ + console.log('Submit','name',name,' value',value); + let data :Partial = {} + if(name==='posX') data['posX'] = value; + if(name==='posY') data['posY'] = value; + if(name==='height') data['height'] = value; + if(name==='width') data['width'] = value; + if(name==='zIndex') data['zIndex'] = value; + + + api_ProconVisualisierungen_Update(id,data); + } + + return ( + <> + + + + + + + + + ); +} + +/* +
{element.posX}
+
{element.posY}
+
{element.height}
+
{element.width}
+
{element.zIndex}
+ */ \ No newline at end of file diff --git a/src/components/rk/anlage/anlageColumn.tsx b/src/components/rk/anlage/anlageColumn.tsx new file mode 100644 index 0000000..7e2d675 --- /dev/null +++ b/src/components/rk/anlage/anlageColumn.tsx @@ -0,0 +1,32 @@ +"use client" +import { api_anlage_setSelected } from "@/actions/rk/api-anlage"; +import { Anlage } from "@/types/rk"; + + + +interface Props { + data: Anlage, + isSelected: boolean +} + +export default function AnlageColumn({ data, isSelected }: Props) { + + + + return
api_anlage_setSelected(data.id)}> +
+ {data.id} +
+
+ {data.anlageNr} +
+ +
+ {data.name} +
+
+ + + +} \ No newline at end of file diff --git a/src/components/rk/elements/elementCreateView.tsx b/src/components/rk/elements/elementCreateView.tsx new file mode 100644 index 0000000..c89db18 --- /dev/null +++ b/src/components/rk/elements/elementCreateView.tsx @@ -0,0 +1,53 @@ +"use client" + +import { Anlage, Element, ExcelProCon, Group } from "@/types/rk"; +import { useEffect, useState } from "react"; +import { ElementEdit } from "./elementEdit"; +import ElementViewItem from "./elementsViewItem"; +import ElementViewListHeader from "./elementsViewListHeader"; +import ElementSelectExcelList from "./elementsSelectExcelList"; +import { ElementEditRow } from "./elementEditRow"; +import { useElements } from "@/hooks/useElements"; + + +interface Props { + analge:Anlage, + elements: Element[], + groups: Group[] +} + +export default function ElementCreateView({ analge,elements:initElements, groups }: Props) { + + const { elements, selectedElement, filterExcelListe, setFilterExcelListe, setSelectedItem } = useElements({ elements: initElements }); + + + + +return ( +
+
+ + setFilterExcelListe(liste)}> + + + {filterExcelListe} + +
+ +
+ {elements.map((item) => + + + + )} +
+
+
+ + + +
+) +} + + diff --git a/src/components/rk/elements/elementEdit.tsx b/src/components/rk/elements/elementEdit.tsx new file mode 100644 index 0000000..f6716d6 --- /dev/null +++ b/src/components/rk/elements/elementEdit.tsx @@ -0,0 +1,141 @@ +"use client"; + +import { useEffect, useState } from "react" +import { useForm } from "react-hook-form" +import { InputTextR } from "@/components/ui/inputTextR"; +import { Element, ElementCreate, emptyElement, Group } from "@/types/rk"; + +import { api_elementDelete, elementCreate, elementUpdate } from "@/actions/rk/api-element"; +import { SelectR } from "@/components/ui/selectR"; + + +interface Params { + groups: Group[] + initData?: Element + debug?: boolean +} + +export const ElementEdit = ({ initData, groups, debug }: Params) => { + + const { control, register, handleSubmit, formState: { errors }, watch, getFieldState, reset } = useForm({ defaultValues: emptyElement, }) + + + const [modeUpdate, setModeUpdate] = useState(false); + const [selGroup, setSelGroup] = useState(); + + const [id, setId] = useState(); + const [error, setError] = useState(); + + useEffect(() => { + if (initData) { + const { id, group, ...rest } = initData; + setId(id); + setModeUpdate(true); + if (initData.group) setSelGroup(() => initData.group.id); + reset({ ...rest, groupId: group ? group.id : undefined }) + } + }, [initData]); + + const onSubmit = async (data: ElementCreate) => { + setError(undefined); + //const { name, isConsumable, description } = data; + if (modeUpdate) { + if (id) { + elementUpdate(id, data); + } else { + setError('Etwas ist falsch, keine ID'); + } + + } else { + elementCreate(data); + } + } + + const handlerDelete = async () => { + if (id) api_elementDelete(id); + } + + const excelListe = [{ id: "keine", value: "" }, + { id: "Aggregate", value: "Aggregate" }, + { id: "Sensoren", value: "Sensoren" }, + { id: "Analogwerte", value: "Analogwerte" }, + { id: "AnlagenVisu", value: "AnlagenVisualisierung" }, + ] + + const options = watch("options") + + return ( +
+
+
+
+ + {initData && initData.group && +
+ {initData?.group.nr}_{initData?.group.name} +
} + +
+ ({ id: g.id, value: g.nr + '_' + g.name }))} {...register("groupId")} > +
+
+
+ + +
+ + + + ({ id: e.id, value: e.value }))} {...register("excelListe")} > + +
+ {error &&
{error}
} + {errors.name &&
{errors.name.message}
} +
+ +
+
Optionen
+
+ {options && + Object.entries(options).map(([key, value]) => ( +
+ + + + {typeof value === "boolean" ? ( +
+ +
+ + ) : ( +
+ +
+ )} + +
)) + } +
+
+
+ + {id && } +
+ +
+ + {debug && +
+
ID: {id}
+
{JSON.stringify(watch(), null, 2)}
+
+ } +
+ + + ) +} diff --git a/src/components/rk/elements/elementEditRow.tsx b/src/components/rk/elements/elementEditRow.tsx new file mode 100644 index 0000000..1aa212e --- /dev/null +++ b/src/components/rk/elements/elementEditRow.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { useEffect, useState } from "react" +import { useForm } from "react-hook-form" +import { InputTextR } from "@/components/ui/inputTextR"; +import { Anlage, Element, ElementCreate, emptyElement, Group } from "@/types/rk"; + +import { api_elementDelete, elementCreate, elementUpdate } from "@/actions/rk/api-element"; +import { SelectR } from "@/components/ui/selectR"; +import { useElement } from "@/hooks/useElement"; +import { Sites } from "@/_config/sites"; +import Toast from "@/components/ui/Toast"; + + +interface Params { + anlage: Anlage, + groups: Group[] + elementIntial?: Element + debug?: boolean +} + +export const ElementEditRow = ({ anlage, elementIntial, groups, debug }: Params) => { + + const { control, register, handleSubmit, formState: { errors }, watch, setFocus, reset } = useForm({ defaultValues: emptyElement, }) + const { proConExcelLists } = useElement({ element: elementIntial }) + const [selGroup, setSelGroup] = useState(); + const [toast, setToast] = useState(null); + + + const [error, setError] = useState(); + + useEffect(() => { + if (elementIntial) { + const { id, group, ...rest } = elementIntial; + + if (elementIntial.group) setSelGroup(() => elementIntial.group.id); + reset({ ...rest, groupId: group ? group.id : undefined }) + } else { + reset({ ...emptyElement, anlageId: anlage.id }) + } + }, [elementIntial]); + + const onSubmit = async (data: ElementCreate) => { + setError(undefined); + //await elementCreate(data); + setToast("Erfolgreich gespeichert!"); + setFocus('excelNr') + } + + + + + const options = watch("options") + + return ( +
+ +
+ {toast && ( + setToast(null)} + /> + )} + +
+
+
+ +
+ + +
+ + + + + +
+ {error &&
{error}
} + {errors.name &&
{errors.name.message}
} +
+ + +
+ + +
+ +
+
+ + {true && +
+
{JSON.stringify(watch(), null, 2)}
+
+ } +
+ + + ) +} diff --git a/src/components/rk/elements/elementSelect.tsx b/src/components/rk/elements/elementSelect.tsx new file mode 100644 index 0000000..d7d9d96 --- /dev/null +++ b/src/components/rk/elements/elementSelect.tsx @@ -0,0 +1,28 @@ +import { Element, Group } from "@/types/rk"; + + +interface Props { + elements: Element[], + onSelect?: (element:Element)=>void +} + +export default function ElementSelect({ elements,onSelect }: Props) { + + const handleSelect = (element:Element)=>{ + if(onSelect) onSelect(element); + } + return ( +
+
+ {elements.map((el) => ( + +
handleSelect(el)} key={el.id}> +
{`${el.group ? el.group.nr:'noGroup'}${el.prefix ? el.prefix:""} ${el.name}`}
+
))} + +
+
+ ) +} + + diff --git a/src/components/rk/elements/elementsSelectExcelList.tsx b/src/components/rk/elements/elementsSelectExcelList.tsx new file mode 100644 index 0000000..9b92026 --- /dev/null +++ b/src/components/rk/elements/elementsSelectExcelList.tsx @@ -0,0 +1,27 @@ +"use client" + +import { ExcelProCon } from "@/types/rk"; +import MonButton from "@/components/ui/MonBotton"; + + +interface Props { + + onSelect:(excelListitem:ExcelProCon)=> void +} + +export default function ElementSelectExcelList({ onSelect }: Props) { + + + return ( +
+ {Object.values(ExcelProCon).map((item) => ( + onSelect(item)}> + {item} + + ))} +
+ + ) +} + + diff --git a/src/components/rk/elements/elementsView.tsx b/src/components/rk/elements/elementsView.tsx new file mode 100644 index 0000000..a047ef5 --- /dev/null +++ b/src/components/rk/elements/elementsView.tsx @@ -0,0 +1,50 @@ +"use client" + +import { Element, ExcelProCon, Group } from "@/types/rk"; +import { useEffect, useState } from "react"; +import { ElementEdit } from "./elementEdit"; +import ElementViewItem from "./elementsViewItem"; +import ElementViewListHeader from "./elementsViewListHeader"; +import ElementSelectExcelList from "./elementsSelectExcelList"; +import { useElements } from "@/hooks/useElements"; + + +interface Props { + elements: Element[], + groups: Group[] +} + +export default function ElementView({ elements: initElements, groups }: Props) { + + const { elements, selectedElement, filterExcelListe, setFilterExcelListe, setSelectedItem } = useElements({ elements: initElements }); + + + return ( +
+
+ setFilterExcelListe(liste)}> + {filterExcelListe} + +
+ +
+ {elements.map((item) => + + + + )} +
+
+
+ + {selectedElement && +
+ +
+ } + +
+ ) +} + + diff --git a/src/components/rk/elements/elementsViewItem.tsx b/src/components/rk/elements/elementsViewItem.tsx new file mode 100644 index 0000000..c27c8e1 --- /dev/null +++ b/src/components/rk/elements/elementsViewItem.tsx @@ -0,0 +1,48 @@ +"use client" + +import { Element, ExcelProCon, Group } from "@/types/rk"; +import { useEffect, useState } from "react"; +import { ElementEdit } from "./elementEdit"; + + +interface Props { + element: Element, + onSelect: (element: Element) => void +} + +export default function ElementViewItem({ element, onSelect }: Props) { + + + const cols = [180, 80, 200, 300, 300, 150]; + + const style = { gridTemplateColumns: cols.map(c => `${c}px`).join(" ") } + + return ( + +
onSelect(element)}> + +
{element.excelListe}
+
{element.excelNr}
+ {element.group ?
{element.group.nr} {element.group.name}
:
-
} +
{element.name}
+ + {/* Type */} + {element.group ?
+ G_{element.group.nr} + {element.prefix != undefined && element.prefix !== "" && "_" + element.prefix} + _{element.group.name} + + {element.funcTxt != undefined && element.funcTxt !== "" && "_" + element.funcTxt} +
:
n.v.
} + {
{element.type}
} + + +
+ + + + + ) +} + + diff --git a/src/components/rk/elements/elementsViewListHeader.tsx b/src/components/rk/elements/elementsViewListHeader.tsx new file mode 100644 index 0000000..86ce610 --- /dev/null +++ b/src/components/rk/elements/elementsViewListHeader.tsx @@ -0,0 +1,40 @@ +"use client" + +import { Element, ExcelProCon, Group } from "@/types/rk"; +import { useEffect, useState } from "react"; +import { ElementEdit } from "./elementEdit"; +import ElementViewItem from "./elementsViewItem"; + + +interface Props { + +} + +export default function ElementViewListHeader({ }: Props) { + + + const cols = [180, 80, 200, 300, 300, 150]; + const style = { gridTemplateColumns: cols.map(c => `${c}px`).join(" ") }; + + const headers = [ + { id: 0, name: "Excel Liste" }, + { id: 1, name: "Excel Nr" }, + { id: 2, name: "Gruppe" }, + { id: 3, name: "Element" }, + { id: 4, name: "ProConName" }, + { id: 5, name: "Typ" }, + + ] + + + + return ( + +
+ {headers.map((col) =>
{col.name}
)} +
+ + ) +} + + diff --git a/src/components/rk/groups/groupSelect.tsx b/src/components/rk/groups/groupSelect.tsx new file mode 100644 index 0000000..aa87373 --- /dev/null +++ b/src/components/rk/groups/groupSelect.tsx @@ -0,0 +1,28 @@ +import { Group } from "@/types/rk"; + + +interface Props { + groups: Group[], + onSelect?: (id:Group)=>void +} + +export default function GroupSelect({ groups,onSelect }: Props) { + + const handleSelect = (id:Group)=>{ + if(onSelect) onSelect(id); + } + return ( +
+
+ {groups.map((group) => ( + +
handleSelect(group)} key={group.id}> +
{`${group.nr} ${group.name}`}
+
))} + +
+
+ ) +} + + diff --git a/src/components/rk/groups/groupView.tsx b/src/components/rk/groups/groupView.tsx new file mode 100644 index 0000000..9fdd104 --- /dev/null +++ b/src/components/rk/groups/groupView.tsx @@ -0,0 +1,65 @@ +"use client" + +import { Group } from "@/types/rk"; +import GroupSelect from "./groupSelect"; +import { useEffect, useState } from "react"; +import GroupViewItem from "./group_ViewItem"; +import GroupFilter from "./group_Filter"; +import { GroupEdit } from "./group_Edit"; + + +interface Props { + groups: Group[], +} + +export default function GroupView({ groups }: Props) { + + const [selectedItem, setSelectedItem] = useState(); + const [groupFilterd, setGroupFilterd] = useState([]); + + useEffect(() => { + setGroupFilterd([...groups]); + if(selectedItem){ + const updatedGroup = groups.find((g)=>g.id==selectedItem.id); + if(updatedGroup) setSelectedItem(()=>updatedGroup); + + } + }, [groups]) + + const handlerOnChangeFilter = (groups: Group[]) => { + setGroupFilterd([...groups]); + } + + const handleSelect = (item: Group) => { + console.log(item); + setSelectedItem(item); + } + + + return ( +
+
+ + +
+
+ {/** Spalte 1 Auswahl Gruppe */} +
+ +
+ + {/** Spalte 2 Anzeige Gruppe details */} +
+ {selectedItem &&
+
edit
+ + {} +
} +
+
+ +
+ ) +} + + diff --git a/src/components/rk/groups/group_Edit.tsx b/src/components/rk/groups/group_Edit.tsx new file mode 100644 index 0000000..9e28b27 --- /dev/null +++ b/src/components/rk/groups/group_Edit.tsx @@ -0,0 +1,124 @@ +"use client"; + +import { useEffect, useState } from "react" +import { useForm } from "react-hook-form" +import { InputTextR } from "@/components/ui/inputTextR"; +import { Anlage, emptyGroup, Group, GroupCreate } from "@/types/rk"; +import { groupCreate, groupUpdate } from "@/actions/rk/api-groups"; + + + +interface Params { + anlage:Anlage + initData?: Group + debug?: boolean +} + +export const GroupEdit = ({anlage, initData, debug }: Params) => { + + const { control, register, handleSubmit, formState: { errors }, watch, getFieldState, reset } = useForm({ defaultValues: emptyGroup, }) + + + const [modeUpdate, setModeUpdate] = useState(false); + const [id, setId] = useState(); + const [error, setError] = useState(); + + useEffect(() => { + if (initData) { + const { id, ...rest } = initData; + setId(id); + setModeUpdate(true); + reset({ ...rest }) + }else{ + reset({ ...emptyGroup,anlageId:anlage.id }) + + } + }, [initData]); + + const onSubmit = async (data: GroupCreate) => { + setError(undefined); + //const { name, isConsumable, description } = data; + if (modeUpdate) { + if (id) { + groupUpdate(id, data); + } else { + setError('Etwas ist falsch, keine ID'); + } + + } else { + console.log('create',data) + groupCreate(data); + } + } + const options = watch("options") + + return ( +
+
+
+
+ + +
+ + + + + +
+ {error &&
{error}
} + {errors.name &&
{errors.name.message}
} +
+ +
+
Optionen
+
+ {options && + Object.entries(options).map(([key, value]) => ( +
+ + + + {typeof value === "boolean" ? ( +
+ +
+ + ) : ( +
+ +
+ )} + +
)) + } +
+
+ + +
+ + {debug && +
+
{watch("name")}
+
touched:{getFieldState("displayName").isTouched ? 'true' : 'false'}
+
{JSON.stringify(watch(), null, 2)}
+
+ } +
+ + + ) +} + +/* +
+
Optionen
+ +
+ +*/ \ No newline at end of file diff --git a/src/components/rk/groups/group_Filter.tsx b/src/components/rk/groups/group_Filter.tsx new file mode 100644 index 0000000..722c1b1 --- /dev/null +++ b/src/components/rk/groups/group_Filter.tsx @@ -0,0 +1,36 @@ +"use client" + +import { Group } from "@/types/rk"; +import { useEffect, useState } from "react"; + + + +interface Props { + groups: Group[], + onChange: (filterdGroups:Group[])=>void +} + +export default function GroupFilter({ groups,onChange }: Props) { + + const [txt,setText] = useState(''); + + useEffect(()=>{ + const filterd = groups.filter((group)=>{ + let found = false; + if(group.name.includes(txt)) found = true; + if(group.nr.includes(txt)) found = true; + return found + }) + onChange(filterd); + },[txt,groups]) + + return ( +
+
Suche
+ setText(e.target.value)} value={txt} className="w-96 border rounded p-2 border-gray-300" /> + +
+ ) +} + + diff --git a/src/components/rk/groups/group_List.tsx b/src/components/rk/groups/group_List.tsx new file mode 100644 index 0000000..fb97dff --- /dev/null +++ b/src/components/rk/groups/group_List.tsx @@ -0,0 +1,37 @@ +"use client" + +import { Group } from "@/types/rk"; +import GroupListItem from "./group_List_Item"; + + +interface Props { + groups: Group[] +} + +export default function GroupList({ groups }: Props) { + + return ( +
+
+
+
ID
+
Name
+
Optionen
+
Aggregate
+
+ {groups.map((group) => ( + + +
+ + + + + +
))} +
+
+ ) +} + + diff --git a/src/components/rk/groups/group_List_Item.tsx b/src/components/rk/groups/group_List_Item.tsx new file mode 100644 index 0000000..422878b --- /dev/null +++ b/src/components/rk/groups/group_List_Item.tsx @@ -0,0 +1,57 @@ +"use client" + +import { Group } from "@/types/rk"; + + + +interface Props { + item: Group +} + +export default function GroupListItem({ item }: Props) { + + return ( + <> +
+ {item.nr} +
+ +
+ {item.name} +
+ +
+ {item.options && + Object.entries(item.options).map(([key, value]) => ( +
+
{key}
+
+ {typeof value === "boolean" ? (value ? "Ja" : "Nein") : value} +
+
+ ))} +
+ +
+ {item.elements &&
+ {item.elements.map((element) => ( +
+ {false && element.prefix &&
+ {element.prefix} +
} + {element.name} +
+ {element.excelNr} +
+ +
))} +
} +
+ + + + + ) +} + + diff --git a/src/components/rk/groups/group_ViewItem.tsx b/src/components/rk/groups/group_ViewItem.tsx new file mode 100644 index 0000000..89414e5 --- /dev/null +++ b/src/components/rk/groups/group_ViewItem.tsx @@ -0,0 +1,131 @@ +"use client" + +import { Group, Peripherie, PeripherieFilterOptionen } from "@/types/rk"; +import { useEffect, useState } from "react"; +import PeripherieListView from "../peripherie/peripherieListView"; +import { PeripherieFindApi } from "@/actions/rk/api-peripherie"; +import { api_GroupAll, api_groupDelete } from "@/actions/rk/api-groups"; + + + +interface Props { + group: Group +} + +export default function GroupViewItem({ group }: Props) { + + const [showEA_Group, setShowEA_Group] = useState(false); + const [showEA_Element, setShowEA_Element] = useState(false); + const [showEA_Diff, setShowEA_Diff] = useState(false); + const [peripheries, setPeripheries] = useState([]); + + useEffect(() => { + setPeripheries([]); + setShowEA_Group(false); + setShowEA_Element(false); + setShowEA_Diff(false); + + }, [group]) + + const handlerShow_Group = () => { + const newMode = !showEA_Group; + setShowEA_Group(newMode) + if (newMode) { + let filter: PeripherieFilterOptionen = { + groupId: group.id + } + PeripherieFindApi(filter).then(setPeripheries); + + } else { + setPeripheries([]) + }; + + }; + const handlerShow_Element = () => { + setShowEA_Element((pre) => !pre) + //if (filterElementId) filter['elementId'] = filterElementId; + if (showEA_Group) { + let filter: PeripherieFilterOptionen = { + // elementId: group..id + }; + + // PeripherieFindApi(filter).then(setPeripheries); + + } + + }; + const handlerShow_Diff = () => { + setShowEA_Diff((pre) => !pre) + + }; + + const handlerDelete = () => { + if(group.id) api_groupDelete(group.id); + + }; + + + return ( +
+
+
Gruppe :
+
{group.nr}
+
{group.name}
+
+ +
+
Anzeige Name :{group.displayName}
+
Info: {group.info}
+
Ort: {group.ort}
+
Type {group.type}
+
+ +
+
Eigenschaften
+ {group.options && + Object.entries(group.options).map(([key, value]) => ( +
+
{key}
+
+ {typeof value === "boolean" ? (value ? "Ja" : "Nein") : value} +
+
+ ))} +
+ +
+
Elemente
+ + {group.elements &&
+ {group.elements.map((element) => ( +
+ {false && element.prefix && +
{element.prefix}
+ } +
{element.name}
+
{element.excelNr}
+ +
))} +
} +
+ +
+ + + + + +
+ {showEA_Group &&
+ Ein-Ausgänge +
} + {showEA_Group &&
+ +
} + + +
+ ) +} + + diff --git a/src/components/rk/groups/group_create.tsx b/src/components/rk/groups/group_create.tsx new file mode 100644 index 0000000..10ddfeb --- /dev/null +++ b/src/components/rk/groups/group_create.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { useEffect, useState } from "react" +import { useFieldArray, useForm } from "react-hook-form" +import { InputTextR } from "@/components/ui/inputTextR"; +import { InputCheckboxR } from "@/components/ui/inputCheckboxR"; +import { Anlage, emptyGroup, Group, GroupCreate } from "@/types/rk"; +import { groupCreate, groupUpdate } from "@/actions/rk/api-groups"; +import { api_anlage_getSelected } from "@/actions/rk/api-anlage"; +import { useFocusManager } from "@/components/ui/editable/useFocusManager"; + + +interface Params { + anlage:Anlage + defaultGroup?: Group + debug?: boolean +} + +export const GroupBulkEdit = ({anlage, defaultGroup, debug }: Params) => { + + const { control, register, handleSubmit, formState: { errors }, watch, getFieldState, reset } = useForm({ defaultValues: emptyGroup, }) + + + + + const [error, setError] = useState(); + + useEffect(() => { + if(defaultGroup){ + const {id,elements,options,...defaults} = defaultGroup; + reset({ ...emptyGroup,...defaults }) + } + + + }, []); + + const onSubmit = async (data: GroupCreate) => { + setError(undefined); + groupCreate(data); + + } + const options = watch("options") + + const focus = useFocusManager(); + + return ( +
+ +
+
+
+ + +
+ + + + + +
+ {error &&
{error}
} + {errors.name &&
{errors.name.message}
} +
+ +
+
Optionen
+
+ {options && + Object.entries(options).map(([key, value]) => ( +
+ + + + {typeof value === "boolean" ? ( +
+ +
+ + ) : ( +
+ +
+ )} + +
)) + } +
+
+ + + +
+ + {debug && +
+
{watch("name")}
+
touched:{getFieldState("displayName").isTouched ? 'true' : 'false'}
+
{JSON.stringify(watch(), null, 2)}
+
+ } +
+ + + ) +} + +/* +
+
Optionen
+ +
+ +*/ \ No newline at end of file diff --git a/src/components/rk/groups/group_edit_list.tsx b/src/components/rk/groups/group_edit_list.tsx new file mode 100644 index 0000000..c2fb4f8 --- /dev/null +++ b/src/components/rk/groups/group_edit_list.tsx @@ -0,0 +1,50 @@ +"use client" + +import { useEditableList } from "@/components/ui/editable-list/useEditableList"; +import { Group } from "@/types/rk"; +import { useRef } from "react"; +import { GroupEditRow } from "./group_edit_row"; + + +interface Props { + data: Group[], +} + + +export const GroupEditList = ({ data }: Props) => { + + + const listRef = useRef(null); + + const list = useEditableList({ + ref: listRef, data: data, onSave: async (group) => { + + //await anlageApiUpdate(anlage.id, { anlageNr: anlage.anlageNr, name: anlage.name }); + console.log(group) + } + }); + + return ( +
+ + + {data.map((a, index) => ( + + list.select(index)} + onSave={list.save} + onCancel={list.cancelEdit} + /> + + ))} +
) +} diff --git a/src/components/rk/groups/group_edit_row.tsx b/src/components/rk/groups/group_edit_row.tsx new file mode 100644 index 0000000..3b8681f --- /dev/null +++ b/src/components/rk/groups/group_edit_row.tsx @@ -0,0 +1,121 @@ +import { EditableRow } from "@/components/ui/editable/EditableRow"; +import { NumberCell } from "@/components/ui/editable/NumberCell"; +import { TextCell } from "@/components/ui/editable/TextCell"; +import { useFocusManager } from "@/components/ui/editable/useFocusManager"; +import { emptyGroup, Group } from "@/types/rk"; +import { useEffect, useState } from "react"; + +interface Props { + data: Group, + + editing: boolean, + selected: boolean, + onSave: (value: Group) => void; + onCancel: () => void; + onClick: () => void; + options?: { + col1_w?: number + col2_w?: number, + col3_w?: number, + col4_w?: number, + col5_w?: number, + col6_w?: number, + col7_w?: number, + full?: boolean, + col?: boolean, + + } +} + +export const GroupEditRow = ({ data, selected, editing, options, onSave, onCancel }: Props) => { + const [id, setId] = useState(''); + const [editData, setEditData] = useState(emptyGroup); + + const focus = useFocusManager(); + + const col1w = options?.col1_w ? options?.col1_w : 100 + const col2w = options?.col2_w ? options?.col2_w : 100 + const col3w = options?.col3_w ? options?.col3_w : 100 + const col4w = options?.col4_w ? options?.col4_w : 100 + + + useEffect(() => { + const { id, ...anlageData } = data; + setEditData(() => ({ ...anlageData })); + setId(() => id); + + }, [data]); + + useEffect(() => { + if (editing) { + focus.focusFirst(); + } + }, [editing]); + + + const handleKeyDown = (e: React.KeyboardEvent) => { + + switch (e.key) { + + case "Enter": + e.preventDefault(); + onSave({ ...data, ...editData }) + break; + + case "Escape": + e.preventDefault(); + onCancel(); + break; + + case "Tab": + e.preventDefault(); + + if (e.shiftKey) focus.focusPrevious(e.currentTarget); + else focus.focusNext(e.currentTarget); + + break; + + } + + + + } + + return ( + + + setEditData((o) => ({ ...o, nr: value }))} + onKey={handleKeyDown} + /> + + setEditData((o) => ({ ...o, name: value }))} + onKey={handleKeyDown} + /> + + setEditData((o) => ({ ...o, funcTxt: value }))} + onKey={handleKeyDown} + /> + + setEditData((o) => ({ ...o, info: value }))} + onKey={handleKeyDown} + /> + + + ) +} diff --git a/src/components/rk/logNote/logNoteList.tsx b/src/components/rk/logNote/logNoteList.tsx new file mode 100644 index 0000000..63c0c9c --- /dev/null +++ b/src/components/rk/logNote/logNoteList.tsx @@ -0,0 +1,27 @@ +"use client" + + + +import { LogNote } from "@/types/rk"; +import { useEffect, useState } from "react"; +import LogNoteListItem from "./logNoteListItem"; + + +interface Props { + notes: LogNote[], +} + +export default function LogNoteList({ notes }: Props) { + + + + + + return ( +
+ {notes.map((item)=>)} +
+ ) +} + + diff --git a/src/components/rk/logNote/logNoteListItem.tsx b/src/components/rk/logNote/logNoteListItem.tsx new file mode 100644 index 0000000..9a54715 --- /dev/null +++ b/src/components/rk/logNote/logNoteListItem.tsx @@ -0,0 +1,35 @@ +"use client" + + + +import { LogNote } from "@/types/rk"; +import { useEffect, useState } from "react"; + + +interface Props { + note: LogNote, +} + +export default function LogNoteListItem({ note }: Props) { + + + + + + return ( +
+ {/** Spalte 1 Topic */} +
+ {note.topic} +
+
+ {note.status} +
+ +
+ + + ) +} + + diff --git a/src/components/rk/logNote/logNoteView.tsx b/src/components/rk/logNote/logNoteView.tsx new file mode 100644 index 0000000..5f81480 --- /dev/null +++ b/src/components/rk/logNote/logNoteView.tsx @@ -0,0 +1,29 @@ +"use client" + + + +import { LogNote } from "@/types/rk"; +import { useEffect, useState } from "react"; +import LogNoteList from "./logNoteList"; + + +interface Props { + notes: LogNote[], +} + +export default function LogNoteView({ notes }: Props) { + + + + + + return ( +
+ + + +
+ ) +} + + diff --git a/src/components/rk/peripherie/peripherieListInput.tsx b/src/components/rk/peripherie/peripherieListInput.tsx new file mode 100644 index 0000000..d23aac8 --- /dev/null +++ b/src/components/rk/peripherie/peripherieListInput.tsx @@ -0,0 +1,76 @@ +"use client" + +import { Group, GroupSubElement, Peripherie, PeripherieFilterOptionen, PeripherieUpdate } from "@/types/rk"; +import { useEffect, useState } from "react"; +import { api_peripherieUpdate, PeripherieFindApi } from "@/actions/rk/api-peripherie"; +import { useStringInput } from "@/hooks/useStringInput"; + + +interface Props { + peripheries: Peripherie + groups: Group[] + +} + +export default function PeripherieListInput({ peripheries, groups }: Props) { + + + + const [filteredGroups, setFilteredGroups] = useState([]); + + const nameInput = useStringInput({ value: peripheries.name, fallback: "", onCommit: (value) => api_peripherieUpdate(peripheries.id, { name: value }) }); + const functionTxtInput = useStringInput({ value: peripheries.functionTxt, fallback: "", onCommit: (value) => api_peripherieUpdate(peripheries.id, { functionTxt: value }) }); + const textInput = useStringInput({ value: peripheries.text, fallback: "", onCommit: (value) => api_peripherieUpdate(peripheries.id, { text: value }) }); + + useEffect(() => { + setFilteredGroups(groups); + }, [groups]) + + const handleChangeGroup = (id: string, value: string) => { + api_peripherieUpdate(id, { groupId: value }) + console.log(id, { groupId: value }); + } + //if(peripheries.excelData?.functionText === peripheries.text) return < > + return ( + + +
+
+ + +
+
+ {peripheries.element?.name} +
+
+ +
+
+ + +
+
+ {peripheries.adrType}{peripheries.adrByte}.{peripheries.adrBit} +
+ +
+ +
+ +
+ +
+ {peripheries.excelData?.functionText} +
+
+ + + ) + +} + + diff --git a/src/components/rk/peripherie/peripherieListView.tsx b/src/components/rk/peripherie/peripherieListView.tsx new file mode 100644 index 0000000..ea18260 --- /dev/null +++ b/src/components/rk/peripherie/peripherieListView.tsx @@ -0,0 +1,42 @@ +"use client" + +import { Element, Group, GroupSubElement, Peripherie, PeripherieFilterOptionen } from "@/types/rk"; +import { useEffect, useState } from "react"; +import { api_peripherieUpdate, PeripherieFindApi } from "@/actions/rk/api-peripherie"; +import PeripherieListInput from "./peripherieListInput"; + + +interface Props { + peripheries:Peripherie[] + groups:Group[] + elemente:Element[] +} + +export default function PeripherieListView({peripheries, groups }: Props) { + + + + const [filteredGroups,setFilteredGroups] = useState([]); + + useEffect(()=>{ + setFilteredGroups(groups); + },[groups]) + + const handleChangeGroup = (id:string,value:string)=>{ + api_peripherieUpdate(id,{groupId:value}) + console.log(id,{groupId:value}); + } + return ( +
+ + {peripheries.map((p) => (
+ + + +
))} + +
+ ) +} + + diff --git a/src/components/rk2/ausgabe/ausgabeUnitInfo.tsx b/src/components/rk2/ausgabe/ausgabeUnitInfo.tsx new file mode 100644 index 0000000..7b75c5e --- /dev/null +++ b/src/components/rk2/ausgabe/ausgabeUnitInfo.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { Anlage } from "@/types/rk"; +import Modal from "@/components/ui/Modal"; +import { GroupResponse } from "@/types/anlagen/groups"; +import { useEffect, useState } from "react"; +import { useGroups } from "@/hooks/rk2/useGroups"; +import { UnitResponse } from "@/types/anlagen/units"; +import { useUnit } from "@/hooks/rk2/useUnit"; +import { ComponentResponse } from "@/types/anlagen/components"; + + + +interface Params { + anlage?: Anlage, + groups: GroupResponse[], + units: UnitResponse[] +} + +export const AusgabeUnitInfo = ({ anlage, groups, units }: Params) => { + + const {getPathToRoot} = useGroups({groups}) + + const getUnitInfo = (unit:UnitResponse) =>{ + let groupId = unit.groupId; + + let uGroupPath = getPathToRoot(groupId); + + let unitName = [unit.prefix,unit.bmk,unit.name] + if(unit.postfix) unitName.push(unit.postfix) + + let typ = unit.type; + + //--- Components + let txt = ""; + txt = uGroupPath.map((g)=>g.name).join('\t')+'\t'; + txt += "\t" + unitName.join('_')+'\t'; + //txt += "\tTyp:" + typ+'\n'; + + //txt += "\t\tKomponenten:"+'\n' + unit.components.forEach((comp)=>{ + let cName:string[] = [] + comp.prefix && cName.push(comp.prefix); + comp.name && cName.push(comp.name + (comp.index ? comp.index.toString():"")); + //comp.index && cName.push(); + comp.postfix && cName.push(comp.postfix); + //,comp.name,comp.index,comp.postfix + + txt += '\t' + cName.join('_')+'' + }) + + return txt; + + } + + const getComponentData = (comp:ComponentResponse) =>{ + const name = [comp.prefix, comp.name,comp.index, comp.postfix] + } + + const getText = ()=>{ + let txt = "" + + units.forEach((u)=> txt += getUnitInfo(u)+`\n`) + return txt; + } + return ( +
+