mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:51:18 +00:00
added customer and detail customer page
This commit is contained in:
parent
6bb645ac01
commit
1c92b0141f
8 changed files with 390 additions and 5 deletions
28
src/app/(main)/kunden/[id]/page.tsx
Normal file
28
src/app/(main)/kunden/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { requireManager } from "@/src/lib/auth/getUser";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
const KundeDetailPage = async ({ params }: Props) => {
|
||||
await requireManager();
|
||||
const { id } = await params;
|
||||
|
||||
return (
|
||||
<div className="p-2">
|
||||
<Link
|
||||
href="/kunden"
|
||||
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground mb-6"
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
Zurück zur Übersicht
|
||||
</Link>
|
||||
<h1 className="text-4xl font-bold mb-2">Kundendetails</h1>
|
||||
<p className="text-muted-foreground text-sm">Kunde ID: {id}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KundeDetailPage;
|
||||
9
src/app/(main)/kunden/page.tsx
Normal file
9
src/app/(main)/kunden/page.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { requireManager } from "@/src/lib/auth/getUser";
|
||||
import KundenView from "@/src/modules/kunden/ui/views/kunden-view";
|
||||
|
||||
const KundenPage = async () => {
|
||||
await requireManager();
|
||||
return <KundenView />;
|
||||
};
|
||||
|
||||
export default KundenPage;
|
||||
|
|
@ -71,13 +71,13 @@
|
|||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar: oklch(1 0 0 / 55%);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0 / 70%);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-border: oklch(0.85 0 0 / 40%);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
|
|
|
|||
116
src/components/ui/table.tsx
Normal file
116
src/components/ui/table.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/src/lib/utils"
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
|
|
@ -129,10 +129,10 @@ const DashboardSidebar = ({ userData, displayName }: DashboardSidebarProps) => {
|
|||
});
|
||||
|
||||
return (
|
||||
<Sidebar>
|
||||
<Sidebar className="border-r-0 bg-white/55 backdrop-blur-xl supports-backdrop-filter:bg-white/45 shadow-[1px_0_0_0_oklch(0.85_0_0/30%)]">
|
||||
<SidebarHeader className="text-sidebar-accent-foreground">
|
||||
<Link href="/" className="flex justify-center items-center gap-2 px-2 pt-2">
|
||||
<Image src={Logo} height={65} alt="Cloud_Clipboard" />
|
||||
<Image src={Logo} height={45} alt="Kumasy Logo" />
|
||||
</Link>
|
||||
</SidebarHeader>
|
||||
|
||||
|
|
|
|||
117
src/modules/kunden/ui/components/kunden-table.tsx
Normal file
117
src/modules/kunden/ui/components/kunden-table.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/src/components/ui/table";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { ArrowRight, User, Mail, Phone, Tag } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { Kunde } from "../views/kunden-view";
|
||||
|
||||
const STATUS_STYLES: Record<Kunde["status"], string> = {
|
||||
Teilgenommen: "bg-green-100 text-green-700",
|
||||
Storniert: "bg-red-100 text-red-600",
|
||||
Bezahlt: "bg-blue-100 text-blue-700",
|
||||
};
|
||||
|
||||
interface KundenTableProps {
|
||||
kunden: Kunde[];
|
||||
}
|
||||
|
||||
const KundenTable = ({ kunden }: KundenTableProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="rounded-xl overflow-hidden border border-white/40 bg-white/60 backdrop-blur-xl shadow-sm supports-backdrop-filter:bg-white/50">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-white/30 hover:bg-white/30 border-b-0">
|
||||
<TableHead className="font-semibold text-gray-700 pt-4 pb-3">
|
||||
<span className="flex items-center gap-2">
|
||||
<User size={14} className="text-gray-400" />
|
||||
Name
|
||||
</span>
|
||||
</TableHead>
|
||||
<TableHead className="hidden md:table-cell font-semibold text-gray-700 pt-4 pb-3">
|
||||
<span className="flex items-center gap-2">
|
||||
<Mail size={14} className="text-gray-400" />
|
||||
E-Mail
|
||||
</span>
|
||||
</TableHead>
|
||||
<TableHead className="hidden lg:table-cell font-semibold text-gray-700 pt-4 pb-3">
|
||||
<span className="flex items-center gap-2">
|
||||
<Phone size={14} className="text-gray-400" />
|
||||
Telefonnummer
|
||||
</span>
|
||||
</TableHead>
|
||||
<TableHead className="font-semibold text-gray-700 pt-4 pb-3">
|
||||
<span className="flex items-center gap-2">
|
||||
<Tag size={14} className="text-gray-400" />
|
||||
Status
|
||||
</span>
|
||||
</TableHead>
|
||||
<TableHead className="w-12" />
|
||||
</TableRow>
|
||||
{/* HR-Trennlinie zwischen Header und Daten */}
|
||||
<tr aria-hidden>
|
||||
<td colSpan={5} className="p-0">
|
||||
<hr className="border-none bg-linear-to-r from-transparent via-gray-300/70 to-transparent" />
|
||||
</td>
|
||||
</tr>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{kunden.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center py-12 text-muted-foreground">
|
||||
Keine Kunden gefunden.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
kunden.map((kunde) => (
|
||||
<TableRow
|
||||
key={kunde.id}
|
||||
className="border-b border-white/30 transition-colors duration-150 hover:bg-white/70 cursor-pointer"
|
||||
onClick={() => router.push(`/kunden/${kunde.id}`)}
|
||||
>
|
||||
<TableCell className="font-medium">{kunde.name}</TableCell>
|
||||
<TableCell className="hidden md:table-cell text-muted-foreground">
|
||||
{kunde.email}
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell text-muted-foreground">
|
||||
{kunde.telefon}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span
|
||||
className={`inline-block text-xs font-semibold px-2.5 py-1 rounded-full ${STATUS_STYLES[kunde.status]}`}
|
||||
>
|
||||
{kunde.status}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="rounded-full hover:bg-white/60"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
router.push(`/kunden/${kunde.id}`);
|
||||
}}
|
||||
>
|
||||
<ArrowRight size={16} />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KundenTable;
|
||||
115
src/modules/kunden/ui/views/kunden-view.tsx
Normal file
115
src/modules/kunden/ui/views/kunden-view.tsx
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Input } from "@/src/components/ui/input";
|
||||
import { Search } from "lucide-react";
|
||||
import KundenTable from "../components/kunden-table";
|
||||
|
||||
const DUMMY_KUNDEN = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Stern Thireau",
|
||||
email: "sthireau0@prilog.org",
|
||||
telefon: "+49 160 14256244",
|
||||
status: "Teilgenommen" as const,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Ford Mckibbin",
|
||||
email: "fmckibbin1@slate.com",
|
||||
telefon: "+49 160 84256328",
|
||||
status: "Storniert" as const,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Foss Roglieri",
|
||||
email: "froglieri2@xing.com",
|
||||
telefon: "+49 160 78745913",
|
||||
status: "Teilgenommen" as const,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "Maurits Elgey",
|
||||
email: "melgey3@blogger.com",
|
||||
telefon: "+49 160 55834625",
|
||||
status: "Teilgenommen" as const,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "Gun Kaasmann",
|
||||
email: "gkaasmann4@economist.com",
|
||||
telefon: "+49 160 32679584",
|
||||
status: "Teilgenommen" as const,
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
name: "Edmund McCrae",
|
||||
email: "emccrae5@woothemes.com",
|
||||
telefon: "+49 160 69321457",
|
||||
status: "Bezahlt" as const,
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
name: "Samuel Totman",
|
||||
email: "stotman6@wisc.edu",
|
||||
telefon: "+49 160 96851428",
|
||||
status: "Teilgenommen" as const,
|
||||
},
|
||||
{
|
||||
id: "8",
|
||||
name: "Patsy Cuardall",
|
||||
email: "pcuardall70@barnesandnoble.com",
|
||||
telefon: "+49 160 38591426",
|
||||
status: "Bezahlt" as const,
|
||||
},
|
||||
{
|
||||
id: "9",
|
||||
name: "Barnaby Carl",
|
||||
email: "bcarl8@alexa.com",
|
||||
telefon: "+49 160 77958426",
|
||||
status: "Storniert" as const,
|
||||
},
|
||||
{
|
||||
id: "10",
|
||||
name: "Lena Bergmann",
|
||||
email: "lbergmann9@spiegel.de",
|
||||
telefon: "+49 160 12345678",
|
||||
status: "Bezahlt" as const,
|
||||
},
|
||||
];
|
||||
|
||||
export type Kunde = (typeof DUMMY_KUNDEN)[number];
|
||||
|
||||
const KundenView = () => {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const filtered = DUMMY_KUNDEN.filter(
|
||||
(k) =>
|
||||
k.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
k.email.toLowerCase().includes(search.toLowerCase()) ||
|
||||
k.telefon.includes(search)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-2">
|
||||
<div className="flex flex-col gap-3 mb-6 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h1 className="text-4xl font-bold">Kunden</h1>
|
||||
<div className="relative w-full sm:w-60">
|
||||
<Search
|
||||
className="absolute left-3 top-1/2 -translate-y-1/2 z-10 pointer-events-none text-muted-foreground"
|
||||
size={16}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Suchen"
|
||||
className="pl-9 bg-white/60 backdrop-blur-xl border-white/40 supports-backdrop-filter:bg-white/50 placeholder:text-gray-400 focus-visible:ring-white/40 focus-visible:bg-white/80"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<KundenTable kunden={filtered} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KundenView;
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 23 KiB |
Loading…
Add table
Add a link
Reference in a new issue