tr]:last:border-b-0",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
+ return (
+
+ )
+}
+
+function TableHead({ className, ...props }: React.ComponentProps<"th">) {
+ return (
+ [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCell({ className, ...props }: React.ComponentProps<"td">) {
+ return (
+ | [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCaption({
+ className,
+ ...props
+}: React.ComponentProps<"caption">) {
+ return (
+
+ )
+}
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+}
diff --git a/internal_frontend/components/ui/textarea.tsx b/internal_frontend/components/ui/textarea.tsx
new file mode 100644
index 0000000..7f21b5e
--- /dev/null
+++ b/internal_frontend/components/ui/textarea.tsx
@@ -0,0 +1,18 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
+ return (
+
+ )
+}
+
+export { Textarea }
diff --git a/internal_frontend/lib/api/callApi.ts b/internal_frontend/lib/api/callApi.ts
new file mode 100644
index 0000000..1b89a1a
--- /dev/null
+++ b/internal_frontend/lib/api/callApi.ts
@@ -0,0 +1,28 @@
+// lib/api/callApi.ts
+import {serverCall} from "@/lib/api/serverCall";
+
+export async function callApi(
+ path: string,
+ method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
+ body?: TRequest
+): Promise {
+ const res = await serverCall(path, method, body);
+
+ const contentType = res.headers.get("content-type") ?? "";
+ const isJson = contentType.includes("application/json");
+
+ const rawBody = isJson ? await res.json() : await res.text();
+
+ console.log(`[api ${path}] Response:`, res.status, rawBody);
+
+ if (!res.ok) {
+ const errorMessage = isJson
+ ? (rawBody?.message ?? rawBody?.errors?.join(", ")) ?? "Unbekannter Fehler"
+ : String(rawBody);
+
+ console.error(`[api ${path}] Error:`, errorMessage);
+ throw new Error(errorMessage);
+ }
+
+ return rawBody as TResponse;
+}
diff --git a/internal_frontend/lib/api/serverCall.ts b/internal_frontend/lib/api/serverCall.ts
new file mode 100644
index 0000000..dcc6519
--- /dev/null
+++ b/internal_frontend/lib/api/serverCall.ts
@@ -0,0 +1,28 @@
+// lib/callBackendApi.ts
+import {getServerSession} from "next-auth";
+import {authOptions} from "@/lib/auth/authOptions";
+
+export async function serverCall(
+ path: string,
+ method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
+ body?: unknown
+): Promise {
+ const url = `${process.env.INTERNAL_BACKEND_URL ?? "http://localhost:8080/api"}${path}`;
+ const session = await getServerSession(authOptions);
+
+ const headers: Record = {
+ "Content-Type": "application/json",
+ };
+
+ if (session != null) {
+ headers["Authorization"] = `Bearer ${session.accessToken}`;
+ }
+
+ console.log("[api] Calling backend API: ", method, url, body);
+
+ return fetch(url, {
+ method,
+ headers,
+ body: body ? JSON.stringify(body) : undefined,
+ });
+}
\ No newline at end of file
diff --git a/internal_frontend/lib/auth/authOptions.ts b/internal_frontend/lib/auth/authOptions.ts
index a8bb1eb..4248f07 100644
--- a/internal_frontend/lib/auth/authOptions.ts
+++ b/internal_frontend/lib/auth/authOptions.ts
@@ -15,10 +15,10 @@ const {
NEXTAUTH_SECRET,
} = process.env;
-if (!KEYCLOAK_CLIENT_ID) throw new Error("Missing KEYCLOAK_CLIENT_ID");
-if (!KEYCLOAK_CLIENT_SECRET) throw new Error("Missing KEYCLOAK_CLIENT_SECRET");
-if (!KEYCLOAK_ISSUER) throw new Error("Missing KEYCLOAK_ISSUER");
-if (!NEXTAUTH_SECRET) throw new Error("Missing NEXTAUTH_SECRET");
+// if (!KEYCLOAK_CLIENT_ID) throw new Error("Missing KEYCLOAK_CLIENT_ID");
+// if (!KEYCLOAK_CLIENT_SECRET) throw new Error("Missing KEYCLOAK_CLIENT_SECRET");
+// if (!KEYCLOAK_ISSUER) throw new Error("Missing KEYCLOAK_ISSUER");
+// if (!NEXTAUTH_SECRET) throw new Error("Missing NEXTAUTH_SECRET");
console.log("[auth] Using Keycloak provider:");
console.log(" - Client ID:", KEYCLOAK_CLIENT_ID);
@@ -42,8 +42,8 @@ async function isTokenValid(token: string): Promise {
export const authOptions: NextAuthOptions = {
providers: [
KeycloakProvider({
- clientId: KEYCLOAK_CLIENT_ID,
- clientSecret: KEYCLOAK_CLIENT_SECRET,
+ clientId: KEYCLOAK_CLIENT_ID as string,
+ clientSecret: KEYCLOAK_CLIENT_SECRET as string,
issuer: KEYCLOAK_ISSUER,
}),
],
diff --git a/internal_frontend/lib/breadcrumb-map.ts b/internal_frontend/lib/breadcrumb-map.ts
index 4a06b70..f14579c 100644
--- a/internal_frontend/lib/breadcrumb-map.ts
+++ b/internal_frontend/lib/breadcrumb-map.ts
@@ -4,5 +4,6 @@ export const breadcrumbMap: Record = {
'settings': 'Settings',
'demo': 'Demo',
'users': 'User Management',
+ 'customers': 'Kundenübersicht',
// Add more mappings as needed
};
\ No newline at end of file
diff --git a/internal_frontend/lib/ui/showError.ts b/internal_frontend/lib/ui/showError.ts
new file mode 100644
index 0000000..2f3f95d
--- /dev/null
+++ b/internal_frontend/lib/ui/showError.ts
@@ -0,0 +1,16 @@
+// lib/ui/showError.ts
+import {toast} from "sonner";
+
+export function showError(error: unknown, fallback = "Ein unbekannter Fehler ist aufgetreten") {
+ let message: string;
+
+ if (error instanceof Error) {
+ message = error.message;
+ } else if (typeof error === "string") {
+ message = error;
+ } else {
+ message = fallback;
+ }
+
+ toast.error(message);
+}
diff --git a/internal_frontend/package-lock.json b/internal_frontend/package-lock.json
index 14af4cd..9def4e5 100644
--- a/internal_frontend/package-lock.json
+++ b/internal_frontend/package-lock.json
@@ -11,9 +11,12 @@
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
+ "@radix-ui/react-label": "^2.1.7",
+ "@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.7",
+ "axios": "^1.10.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.22.0",
@@ -24,7 +27,9 @@
"next-themes": "^0.4.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
- "tailwind-merge": "^3.3.1"
+ "sonner": "^2.0.6",
+ "tailwind-merge": "^3.3.1",
+ "zustand": "^5.0.6"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
@@ -1308,6 +1313,29 @@
}
}
},
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
+ "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-menu": {
"version": "2.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz",
@@ -1451,6 +1479,30 @@
}
}
},
+ "node_modules/@radix-ui/react-progress": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz",
+ "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz",
@@ -2892,6 +2944,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -2918,6 +2976,17 @@
"node": ">=4"
}
},
+ "node_modules/axios": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
+ "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -2993,7 +3062,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -3149,6 +3217,18 @@
"simple-swizzle": "^0.2.2"
}
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -3309,6 +3389,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@@ -3342,7 +3431,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -3447,7 +3535,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -3457,7 +3544,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -3495,7 +3581,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -3508,7 +3593,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -4115,6 +4199,26 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -4131,6 +4235,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/form-data": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
+ "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/framer-motion": {
"version": "12.22.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.22.0.tgz",
@@ -4162,7 +4282,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -4203,7 +4322,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -4237,7 +4355,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@@ -4325,7 +4442,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -4404,7 +4520,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -4417,7 +4532,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -4433,7 +4547,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -5393,7 +5506,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -5423,6 +5535,27 @@
"node": ">=8.6"
}
},
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -6050,6 +6183,12 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -6572,6 +6711,16 @@
"is-arrayish": "^0.3.1"
}
},
+ "node_modules/sonner": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.6.tgz",
+ "integrity": "sha512-yHFhk8T/DK3YxjFQXIrcHT1rGEeTLliVzWbO0xN8GberVun2RiBnxAjXAYpZrqwEVHBG9asI/Li8TAAhN9m59Q==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
+ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -7305,6 +7454,35 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zustand": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.6.tgz",
+ "integrity": "sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/internal_frontend/package.json b/internal_frontend/package.json
index be8a24d..d86923e 100644
--- a/internal_frontend/package.json
+++ b/internal_frontend/package.json
@@ -12,9 +12,12 @@
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
+ "@radix-ui/react-label": "^2.1.7",
+ "@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.7",
+ "axios": "^1.10.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.22.0",
@@ -25,7 +28,9 @@
"next-themes": "^0.4.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
- "tailwind-merge": "^3.3.1"
+ "sonner": "^2.0.6",
+ "tailwind-merge": "^3.3.1",
+ "zustand": "^5.0.6"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
diff --git a/internal_frontend/services/customers/dtos/createCustomer.dto.ts b/internal_frontend/services/customers/dtos/createCustomer.dto.ts
new file mode 100644
index 0000000..e2c75e6
--- /dev/null
+++ b/internal_frontend/services/customers/dtos/createCustomer.dto.ts
@@ -0,0 +1,19 @@
+export interface PhoneNumberDto {
+ number: string;
+ note: string;
+}
+
+export interface NoteDto {
+ text: string;
+}
+
+export interface CreateCustomerDto {
+ email: string;
+ name: string;
+ companyName: string;
+ phoneNumbers: PhoneNumberDto[];
+ street: string;
+ zip: string;
+ city: string;
+ notes: NoteDto[];
+}
diff --git a/internal_frontend/services/customers/entities/customer.ts b/internal_frontend/services/customers/entities/customer.ts
new file mode 100644
index 0000000..ce194c9
--- /dev/null
+++ b/internal_frontend/services/customers/entities/customer.ts
@@ -0,0 +1,32 @@
+export interface PhoneNumber {
+ number: string;
+ note: string;
+ createdBy: string;
+ createdAt: string;
+ updatedBy: string;
+ updatedAt: string;
+}
+
+export interface Note {
+ text: string;
+ createdBy: string;
+ createdAt: string;
+ updatedBy: string;
+ updatedAt: string;
+}
+
+export interface Customer {
+ id: string;
+ email: string;
+ name: string;
+ companyName: string;
+ phoneNumbers: PhoneNumber[];
+ street: string;
+ zip: string;
+ city: string;
+ notes: Note[];
+ createdBy: string;
+ createdAt: string;
+ updatedBy: string;
+ updatedAt: string;
+}
diff --git a/internal_frontend/services/customers/repositories/customerRepository.ts b/internal_frontend/services/customers/repositories/customerRepository.ts
new file mode 100644
index 0000000..843b40e
--- /dev/null
+++ b/internal_frontend/services/customers/repositories/customerRepository.ts
@@ -0,0 +1,13 @@
+import {Customer} from "@/services/customers/entities/customer";
+import {CreateCustomerDto} from "@/services/customers/dtos/createCustomer.dto";
+import {callApi} from "@/lib/api/callApi";
+
+export class CustomerRepository {
+ static async getAll(): Promise {
+ return await callApi("/customers", "GET");
+ }
+
+ static async create(payload: CreateCustomerDto): Promise {
+ await callApi("/customers", "POST", payload);
+ }
+}
diff --git a/internal_frontend/services/customers/usecases/addCustomer.ts b/internal_frontend/services/customers/usecases/addCustomer.ts
new file mode 100644
index 0000000..d124836
--- /dev/null
+++ b/internal_frontend/services/customers/usecases/addCustomer.ts
@@ -0,0 +1,21 @@
+"use server";
+
+import {CreateCustomerDto} from "@/services/customers/dtos/createCustomer.dto";
+import {CustomerRepository} from "@/services/customers/repositories/customerRepository";
+
+export async function addCustomer(params: CreateCustomerDto): Promise {
+ const {email, name, companyName, street, zip, city, phoneNumbers, notes} = params;
+
+ const payload: CreateCustomerDto = {
+ email,
+ name,
+ companyName,
+ street,
+ zip,
+ city,
+ phoneNumbers,
+ notes: notes.map(({text}) => ({text})),
+ };
+
+ await CustomerRepository.create(payload);
+}
diff --git a/internal_frontend/services/customers/usecases/validateCustomer.ts b/internal_frontend/services/customers/usecases/validateCustomer.ts
new file mode 100644
index 0000000..7fa5364
--- /dev/null
+++ b/internal_frontend/services/customers/usecases/validateCustomer.ts
@@ -0,0 +1,15 @@
+"use server";
+
+import {callApi} from "@/lib/api/callApi";
+import {Customer} from "@/app/customers/page";
+import {customerRoutes} from "@/app/api/customers/customerRoutes";
+
+export async function validateCustomer(input: {
+ email: string;
+ companyName: string;
+ street: string;
+ zip: string;
+ city: string;
+}): Promise {
+ return await callApi(customerRoutes.validate, "POST", input);
+}
\ No newline at end of file
|