Dashboard und Benutzerverwaltung
This commit is contained in:
parent
c4d27a1187
commit
4cf671e32d
16 changed files with 684 additions and 79 deletions
23
frontend/athena/components/Header.tsx
Normal file
23
frontend/athena/components/Header.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
export default function Header() {
|
||||
|
||||
return (
|
||||
|
||||
<header className="h-20 bg-white border-b flex items-center justify-between px-8">
|
||||
|
||||
<h1 className="text-3xl font-bold">
|
||||
|
||||
Dashboard
|
||||
|
||||
</h1>
|
||||
|
||||
<div className="font-semibold">
|
||||
|
||||
admin.schubert
|
||||
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
86
frontend/athena/components/Sidebar.tsx
Normal file
86
frontend/athena/components/Sidebar.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import Link from "next/link";
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Users,
|
||||
Wrench,
|
||||
Package,
|
||||
FileText,
|
||||
Settings,
|
||||
ShoppingCart,
|
||||
} from "lucide-react";
|
||||
|
||||
const menu = [
|
||||
{
|
||||
icon: LayoutDashboard,
|
||||
name: "Dashboard",
|
||||
href: "/",
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
name: "Benutzer",
|
||||
href: "/users",
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
name: "Kunden",
|
||||
href: "/customers",
|
||||
},
|
||||
{
|
||||
icon: Wrench,
|
||||
name: "Reparaturen",
|
||||
href: "/repairs",
|
||||
},
|
||||
{
|
||||
icon: Package,
|
||||
name: "Lager",
|
||||
href: "/inventory",
|
||||
},
|
||||
{
|
||||
icon: ShoppingCart,
|
||||
name: "eBay",
|
||||
href: "/ebay",
|
||||
},
|
||||
{
|
||||
icon: FileText,
|
||||
name: "Dokumente",
|
||||
href: "/documents",
|
||||
},
|
||||
{
|
||||
icon: Settings,
|
||||
name: "Einstellungen",
|
||||
href: "/settings",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Sidebar() {
|
||||
return (
|
||||
<aside className="w-64 h-screen bg-slate-900 text-white flex flex-col">
|
||||
|
||||
<div className="p-6 border-b border-slate-800">
|
||||
<h1 className="text-3xl font-bold">
|
||||
🏛 Olympus
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-4 space-y-2">
|
||||
|
||||
{menu.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className="flex items-center gap-3 rounded-lg px-4 py-3 hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
<item.icon size={20} />
|
||||
<span>{item.name}</span>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
</nav>
|
||||
|
||||
<div className="border-t border-slate-800 p-4 text-sm text-slate-400">
|
||||
Olympus ERP v0.1
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
26
frontend/athena/components/StatCard.tsx
Normal file
26
frontend/athena/components/StatCard.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
value: ReactNode;
|
||||
};
|
||||
|
||||
export default function StatCard({ title, value }: Props) {
|
||||
|
||||
return (
|
||||
|
||||
<div className="bg-white rounded-xl shadow p-6">
|
||||
|
||||
<p className="text-gray-500">
|
||||
{title}
|
||||
</p>
|
||||
|
||||
<h2 className="text-5xl font-bold mt-4">
|
||||
{value}
|
||||
</h2>
|
||||
|
||||
</div>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
21
frontend/athena/components/UserCounter.tsx
Normal file
21
frontend/athena/components/UserCounter.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
|
||||
export default function UserCounter() {
|
||||
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
api.get("/users")
|
||||
.then(res => setCount(res.data.length))
|
||||
.catch(console.error);
|
||||
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>{count}</>
|
||||
);
|
||||
}
|
||||
160
frontend/athena/components/ui/dialog.tsx
Normal file
160
frontend/athena/components/ui/dialog.tsx
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: DialogPrimitive.Backdrop.Props) {
|
||||
return (
|
||||
<DialogPrimitive.Backdrop
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: DialogPrimitive.Popup.Props & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Popup
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2"
|
||||
size="icon-sm"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<XIcon
|
||||
/>
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Popup>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({
|
||||
className,
|
||||
showCloseButton = false,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close render={<Button variant="outline" />}>
|
||||
Close
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn(
|
||||
"font-heading text-base leading-none font-medium",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: DialogPrimitive.Description.Props) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn(
|
||||
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
20
frontend/athena/components/ui/input.tsx
Normal file
20
frontend/athena/components/ui/input.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import * as React from "react"
|
||||
import { Input as InputPrimitive } from "@base-ui/react/input"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<InputPrimitive
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
20
frontend/athena/components/ui/label.tsx
Normal file
20
frontend/athena/components/ui/label.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Label({ className, ...props }: React.ComponentProps<"label">) {
|
||||
return (
|
||||
<label
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
133
frontend/athena/components/users/UserDialog.tsx
Normal file
133
frontend/athena/components/users/UserDialog.tsx
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
import { api } from "@/lib/api";
|
||||
|
||||
export default function UserDialog() {
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const [username, setUsername] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
async function saveUser() {
|
||||
|
||||
console.log("Speichern geklickt");
|
||||
|
||||
try {
|
||||
|
||||
const response = await api.post("/users/", {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
console.log(response.data);
|
||||
|
||||
setOpen(false);
|
||||
|
||||
window.location.reload();
|
||||
|
||||
} catch (error) {
|
||||
|
||||
console.error(error);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<>
|
||||
|
||||
<Button onClick={() => setOpen(true)}>
|
||||
|
||||
+ Neuer Benutzer
|
||||
|
||||
</Button>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
|
||||
<DialogContent>
|
||||
|
||||
<DialogHeader>
|
||||
|
||||
<DialogTitle>
|
||||
|
||||
Neuer Benutzer
|
||||
|
||||
</DialogTitle>
|
||||
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
|
||||
<div>
|
||||
|
||||
<Label>Benutzername</Label>
|
||||
|
||||
<Input
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
<Label>E-Mail</Label>
|
||||
|
||||
<Input
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
<Label>Passwort</Label>
|
||||
|
||||
<Input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
||||
<Button onClick={saveUser}>
|
||||
|
||||
Speichern
|
||||
|
||||
</Button>
|
||||
|
||||
</DialogFooter>
|
||||
|
||||
</DialogContent>
|
||||
|
||||
</Dialog>
|
||||
|
||||
</>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
7
frontend/athena/components/users/UserForm.tsx
Normal file
7
frontend/athena/components/users/UserForm.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default function UserForm() {
|
||||
return (
|
||||
<div>
|
||||
UserForm
|
||||
</div>
|
||||
);
|
||||
}
|
||||
63
frontend/athena/components/users/UserTable.tsx
Normal file
63
frontend/athena/components/users/UserTable.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
"use client";
|
||||
|
||||
import { User } from "@/types/user";
|
||||
|
||||
type Props = {
|
||||
users: User[];
|
||||
};
|
||||
|
||||
export default function UserTable({ users }: Props) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-xl border bg-white shadow">
|
||||
|
||||
<table className="w-full">
|
||||
|
||||
<thead className="bg-slate-100">
|
||||
|
||||
<tr>
|
||||
|
||||
<th className="p-4 text-left">ID</th>
|
||||
<th className="p-4 text-left">Benutzername</th>
|
||||
<th className="p-4 text-left">E-Mail</th>
|
||||
<th className="p-4 text-center">Aktionen</th>
|
||||
|
||||
</tr>
|
||||
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
{users.map((user) => (
|
||||
|
||||
<tr
|
||||
key={user.id}
|
||||
className="border-t hover:bg-slate-50"
|
||||
>
|
||||
|
||||
<td className="p-4">{user.id}</td>
|
||||
<td className="p-4">{user.username}</td>
|
||||
<td className="p-4">{user.email}</td>
|
||||
|
||||
<td className="p-4 text-center">
|
||||
|
||||
<button className="mr-3 rounded bg-blue-600 px-3 py-1 text-white hover:bg-blue-700">
|
||||
Bearbeiten
|
||||
</button>
|
||||
|
||||
<button className="rounded bg-red-600 px-3 py-1 text-white hover:bg-red-700">
|
||||
Löschen
|
||||
</button>
|
||||
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
))}
|
||||
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
6
frontend/athena/components/utils.ts
Normal file
6
frontend/athena/components/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue