Refactor navigation structure and API routes

- Centralize user menu, sidebar items, and breadcrumb logic.
- Map consistent API endpoints in `customerRoutes`.
- Replace inline route definitions with reusable constants.
- Refactor auth configuration file location.
- Improve `<Link>` usage to replace static `<a>` elements.
- Adjust sidebar and dropdown components to use dynamic navigation configurations.
This commit is contained in:
2025-07-07 19:49:58 +02:00
parent 7ba92dc66c
commit e42b352216
15 changed files with 127 additions and 88 deletions

View File

@@ -1,5 +1,5 @@
import NextAuth from "next-auth"; import NextAuth from "next-auth";
import {authOptions} from "@/lib/auth/authOptions"; import {authOptions} from "@/lib/api/auth/authOptions";
const handler = NextAuth(authOptions); const handler = NextAuth(authOptions);
export {handler as GET, handler as POST}; export {handler as GET, handler as POST};

View File

@@ -1,9 +1,10 @@
import {NextRequest, NextResponse} from "next/server"; import {NextRequest, NextResponse} from "next/server";
import {serverCall} from "@/lib/api/serverCall"; import {serverCall} from "@/lib/api/serverCall";
import {customerRoutes} from "@/app/api/customers/customerRoutes";
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
const id = request.url.split('/').pop(); const id = request.url.split('/').pop();
const response = await serverCall(`/customers/${id}`, "GET"); const response = await serverCall(customerRoutes.getById(id!), "GET");
if (!response.ok) { if (!response.ok) {
return NextResponse.json({error: "Customer not found"}, {status: 404}); return NextResponse.json({error: "Customer not found"}, {status: 404});

View File

@@ -1,4 +1,5 @@
export const customerRoutes = { export const customerRoutes = {
create: "/customers", create: "/customers",
validate: "/customers/validate", validate: "/customers/validate",
getById: (id: string) => `/customers/${id}`,
}; };

View File

@@ -1,14 +1,15 @@
import {NextRequest, NextResponse} from "next/server"; import {NextRequest, NextResponse} from "next/server";
import {serverCall} from "@/lib/api/serverCall"; import {serverCall} from "@/lib/api/serverCall";
import {customerRoutes} from "@/app/api/customers/customerRoutes";
export async function GET() { export async function GET() {
const data = await serverCall("/customers", "GET"); const data = await serverCall(customerRoutes.create, "GET");
const customers = await data.json(); const customers = await data.json();
return NextResponse.json(customers); return NextResponse.json(customers);
} }
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
const body = await req.json() const body = await req.json()
const result = await serverCall("/customers", "POST", body); const result = await serverCall(customerRoutes.create, "POST", body);
return NextResponse.json(result.json()); return NextResponse.json(result.json());
} }

View File

@@ -1,7 +1,6 @@
"use client"; "use client";
import {useState, useEffect, useMemo} from "react"; import {useState, useEffect, useMemo} from "react";
import {useRouter} from "next/navigation";
import {Button} from "@/components/ui/button"; import {Button} from "@/components/ui/button";
import {Input} from "@/components/ui/input"; import {Input} from "@/components/ui/input";
import {Card, CardContent} from "@/components/ui/card"; import {Card, CardContent} from "@/components/ui/card";
@@ -25,9 +24,9 @@ import {ArrowRight} from "lucide-react";
import {NewCustomerModal} from "@/components/customers/modal/NewCustomerModal"; import {NewCustomerModal} from "@/components/customers/modal/NewCustomerModal";
import axios from "axios"; import axios from "axios";
import {Customer} from "@/services/customers/entities/customer"; import {Customer} from "@/services/customers/entities/customer";
import Link from "next/link";
export default function CustomersPage() { export default function CustomersPage() {
const router = useRouter();
const [customers, setCustomers] = useState<Customer[]>([]); const [customers, setCustomers] = useState<Customer[]>([]);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -50,7 +49,7 @@ export default function CustomersPage() {
}, []); }, []);
const filtered = useMemo(() => { const filtered = useMemo(() => {
if(customers.length === 0) return []; if (customers.length === 0) return [];
return customers.filter( return customers.filter(
(c) => (c) =>
@@ -143,13 +142,11 @@ export default function CustomersPage() {
<TableCell>{customer.city}</TableCell> <TableCell>{customer.city}</TableCell>
<TableCell>{new Date(customer.createdAt).toLocaleString()}</TableCell> <TableCell>{new Date(customer.createdAt).toLocaleString()}</TableCell>
<TableCell> <TableCell>
<Button <Link href={`/customers/${customer.id}`}>
variant="ghost" <Button variant="ghost" size="icon">
size="icon" <ArrowRight className="w-4 h-4"/>
onClick={() => router.push(`/customers/${customer.id}`)} </Button>
> </Link>
<ArrowRight className="w-4 h-4"/>
</Button>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}

View File

@@ -8,7 +8,7 @@ import {Separator} from "@/components/ui/separator";
import {DynamicBreadcrumb} from "@/components/dynamic-breadcrumb"; import {DynamicBreadcrumb} from "@/components/dynamic-breadcrumb";
import {getServerSession} from "next-auth"; import {getServerSession} from "next-auth";
import LoginScreen from "@/components/login-screen"; import LoginScreen from "@/components/login-screen";
import {authOptions} from "@/lib/auth/authOptions"; import {authOptions} from "@/lib/api/auth/authOptions";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Internal | Rhein Software", title: "Internal | Rhein Software",

View File

@@ -1,13 +1,11 @@
import Link from 'next/link';
import { import {
AppWindowIcon,
ChevronUp, ChevronUp,
ChevronRight, ChevronRight,
Home,
Scale, Scale,
User2, User2,
Settings, LayoutDashboard Settings,
} from "lucide-react"; } from "lucide-react";
import { import {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
@@ -23,40 +21,20 @@ import {
SidebarMenuSubItem, SidebarMenuSubItem,
SidebarMenuSubButton, SidebarMenuSubButton,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { import {
Collapsible, Collapsible,
CollapsibleTrigger, CollapsibleTrigger,
CollapsibleContent, CollapsibleContent,
} from "@/components/ui/collapsible"; } from "@/components/ui/collapsible";
import {rheinItems, customerItems, kanzleiItems} from "@/lib/navigation/sidebar-items";
import {userMenuItems} from "@/lib/navigation/user-menu-items";
const rheinItems = [
{
title: "Dashboard",
url: "/",
icon: Home,
},
{
title: "Apps",
url: "/apps",
icon: AppWindowIcon,
},
];
const customerItems = [
{
title: "Kundenübersicht",
url: "/customers",
icon: LayoutDashboard,
},
];
export function AppSidebar() { export function AppSidebar() {
return ( return (
@@ -74,10 +52,10 @@ export function AppSidebar() {
asChild asChild
className="hover:bg-accent hover:text-accent-foreground" className="hover:bg-accent hover:text-accent-foreground"
> >
<a href={item.url}> <Link href={item.url}>
<item.icon/> <item.icon/>
<span>{item.title}</span> <span>{item.title}</span>
</a> </Link>
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
))} ))}
@@ -95,10 +73,10 @@ export function AppSidebar() {
asChild asChild
className="hover:bg-accent hover:text-accent-foreground" className="hover:bg-accent hover:text-accent-foreground"
> >
<a href={item.url}> <Link href={item.url}>
<item.icon/> <item.icon/>
<span>{item.title}</span> <span>{item.title}</span>
</a> </Link>
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
))} ))}
@@ -117,11 +95,10 @@ export function AppSidebar() {
asChild asChild
className="hover:bg-accent hover:text-accent-foreground" className="hover:bg-accent hover:text-accent-foreground"
> >
<Link href="/demo/settings">
<a href="/demo/settings">
<Settings/> <Settings/>
<span>Demo Settings</span> <span>Demo Settings</span>
</a> </Link>
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
</SidebarMenu> </SidebarMenu>
@@ -144,21 +121,15 @@ export function AppSidebar() {
<CollapsibleContent> <CollapsibleContent>
<SidebarMenuSub className="ml-4 border-l border-border pl-4 flex flex-col gap-y-1"> <SidebarMenuSub className="ml-4 border-l border-border pl-4 flex flex-col gap-y-1">
<SidebarMenuSubItem> {kanzleiItems.map((item) => (
<SidebarMenuSubButton href="/demo/kanzlei/steuer"> <SidebarMenuSubItem key={item.title}>
Steuer <SidebarMenuSubButton asChild>
</SidebarMenuSubButton> <Link href={item.url}>
</SidebarMenuSubItem> {item.title}
<SidebarMenuSubItem> </Link>
<SidebarMenuSubButton href="/demo/kanzlei/rechtsanwalt"> </SidebarMenuSubButton>
Rechtsanwalt </SidebarMenuSubItem>
</SidebarMenuSubButton> ))}
</SidebarMenuSubItem>
<SidebarMenuSubItem>
<SidebarMenuSubButton href="/demo/kanzlei/bilanzbuchhalter">
Bilanzbuchhalter
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub> </SidebarMenuSub>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
@@ -183,15 +154,14 @@ export function AppSidebar() {
side="top" side="top"
className="w-[--radix-popper-anchor-width]" className="w-[--radix-popper-anchor-width]"
> >
<DropdownMenuItem> {userMenuItems.map((item) => (
<span>Account</span> <DropdownMenuItem key={item.title} asChild>
</DropdownMenuItem> <Link href={item.url}>
<DropdownMenuItem> {item.icon && <item.icon className="mr-2 h-4 w-4"/>}
<span>Billing</span> <span>{item.title}</span>
</DropdownMenuItem> </Link>
<DropdownMenuItem> </DropdownMenuItem>
<span>Sign out</span> ))}
</DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</SidebarMenuItem> </SidebarMenuItem>

View File

@@ -11,8 +11,8 @@ import {
} from '@/components/ui/breadcrumb'; } from '@/components/ui/breadcrumb';
import {Skeleton} from "@/components/ui/skeleton"; import {Skeleton} from "@/components/ui/skeleton";
import React, {useEffect, useState} from 'react'; import React, {useEffect, useState} from 'react';
import {getBreadcrumbs} from '@/utils/BreadcrumbUtils'; import {getBreadcrumbs} from '@/services/navigation/breadcrumb-utils';
import {breadcrumbResolvers} from "@/lib/breadcrumb-map"; import {breadcrumbResolvers} from "@/lib/navigation/breadcrumb-map";
interface ResolvedBreadcrumb { interface ResolvedBreadcrumb {
href: string; href: string;

View File

@@ -15,10 +15,10 @@ const {
NEXTAUTH_SECRET, NEXTAUTH_SECRET,
} = process.env; } = process.env;
// if (!KEYCLOAK_CLIENT_ID) throw new Error("Missing KEYCLOAK_CLIENT_ID"); if (!KEYCLOAK_CLIENT_ID) throw new Error("Missing KEYCLOAK_CLIENT_ID");
// if (!KEYCLOAK_CLIENT_SECRET) throw new Error("Missing KEYCLOAK_CLIENT_SECRET"); if (!KEYCLOAK_CLIENT_SECRET) throw new Error("Missing KEYCLOAK_CLIENT_SECRET");
// if (!KEYCLOAK_ISSUER) throw new Error("Missing KEYCLOAK_ISSUER"); if (!KEYCLOAK_ISSUER) throw new Error("Missing KEYCLOAK_ISSUER");
// if (!NEXTAUTH_SECRET) throw new Error("Missing NEXTAUTH_SECRET"); if (!NEXTAUTH_SECRET) throw new Error("Missing NEXTAUTH_SECRET");
console.log("[auth] Using Keycloak provider:"); console.log("[auth] Using Keycloak provider:");
console.log(" - Client ID:", KEYCLOAK_CLIENT_ID); console.log(" - Client ID:", KEYCLOAK_CLIENT_ID);
@@ -42,8 +42,8 @@ async function isTokenValid(token: string): Promise<boolean> {
export const authOptions: NextAuthOptions = { export const authOptions: NextAuthOptions = {
providers: [ providers: [
KeycloakProvider({ KeycloakProvider({
clientId: KEYCLOAK_CLIENT_ID as string, clientId: KEYCLOAK_CLIENT_ID,
clientSecret: KEYCLOAK_CLIENT_SECRET as string, clientSecret: KEYCLOAK_CLIENT_SECRET,
issuer: KEYCLOAK_ISSUER, issuer: KEYCLOAK_ISSUER,
}), }),
], ],

View File

@@ -1,6 +1,6 @@
// lib/callBackendApi.ts // lib/callBackendApi.ts
import {getServerSession} from "next-auth"; import {getServerSession} from "next-auth";
import {authOptions} from "@/lib/auth/authOptions"; import {authOptions} from "@/lib/api/auth/authOptions";
export async function serverCall( export async function serverCall(
path: string, path: string,

View File

@@ -1,20 +1,19 @@
// lib/breadcrumb-map.ts import {customerRoutes} from "@/app/api/customers/customerRoutes";
export const breadcrumbMap: Record<string, string> = { export const breadcrumbMap: Record<string, string> = {
'dashboard': 'Dashboard', 'dashboard': 'Dashboard',
'settings': 'Settings', 'settings': 'Settings',
'demo': 'Demo', 'demo': 'Demo',
'users': 'User Management', 'users': 'User Management',
'customers': 'Kundenübersicht', 'customers': 'Kundenübersicht',
// Add more mappings as needed
}; };
export const breadcrumbResolvers: Record<string, (id: string) => Promise<string>> = { export const breadcrumbResolvers: Record<string, (id: string) => Promise<string>> = {
"customers": async (id: string) => { "customers": async (id: string) => {
const res = await fetch(`/api/customers/${id}`, {cache: "no-store"}); const res = await fetch(`/api${customerRoutes.getById(id)}`, {cache: "no-store"});
const customer = await res .json(); const customer = await res.json();
if (customer.companyName) return `Firma: ${customer.companyName}`; if (customer.companyName) return `Firma: ${customer.companyName}`;
if (customer.name) return `Name: ${customer.name}`; if (customer.name) return `Name: ${customer.name}`;
return `ID: ${id}`; return `ID: ${id}`;
}, },
// Add more mappings as needed
}; };

View File

@@ -0,0 +1,38 @@
import {AppWindowIcon, Home, LayoutDashboard} from "lucide-react";
import {MenuItem, SubMenuItem} from "@/types/navigation/sidebar";
export const rheinItems: MenuItem[] = [
{
title: "Dashboard",
url: "/",
icon: Home,
},
{
title: "Apps",
url: "/apps",
icon: AppWindowIcon,
},
];
export const customerItems: MenuItem[] = [
{
title: "Kundenübersicht",
url: "/customers",
icon: LayoutDashboard,
},
];
export const kanzleiItems: SubMenuItem[] = [
{
title: "Steuer",
url: "/demo/kanzlei/steuer",
},
{
title: "Rechtsanwalt",
url: "/demo/kanzlei/rechtsanwalt",
},
{
title: "Bilanzbuchhalter",
url: "/demo/kanzlei/bilanzbuchhalter",
},
];

View File

@@ -0,0 +1,20 @@
import {LogOut, Settings} from "lucide-react";
export interface UserMenuItem {
title: string;
url: string;
icon?: typeof Settings;
}
export const userMenuItems: UserMenuItem[] = [
{
title: "Settings",
url: "/settings",
icon: Settings,
},
{
title: "Ausloggen",
url: "/api/auth/signout",
icon: LogOut,
},
];

View File

@@ -1,4 +1,4 @@
import {breadcrumbMap} from "@/lib/breadcrumb-map"; import {breadcrumbMap} from "@/lib/navigation/breadcrumb-map";
export interface Breadcrumb { export interface Breadcrumb {
href: string; href: string;

View File

@@ -0,0 +1,12 @@
import {LucideIcon} from "lucide-react";
export interface MenuItem {
title: string;
url: string;
icon: LucideIcon;
}
export interface SubMenuItem {
title: string;
url: string;
}