ui: add protected dashboard CRUD screens and assignment docs
This commit is contained in:
parent
bf42037619
commit
cb0d4d7809
24 changed files with 2662 additions and 101 deletions
103
apps/web/src/routes/dashboard.notes.tsx
Normal file
103
apps/web/src/routes/dashboard.notes.tsx
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { Button } from "@minmon/ui/components/button";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { PageSection, QueryStateCard, ResourceListCard } from "@/components/dashboard-ui";
|
||||
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
|
||||
|
||||
export const Route = createFileRoute("/dashboard/notes")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const notes = useQuery(trpc.notes.list.queryOptions());
|
||||
const servers = useQuery(trpc.servers.list.queryOptions());
|
||||
const removeNote = useMutation({
|
||||
mutationFn: async (id: string) => trpcClient.notes.delete.mutate({ id }),
|
||||
onSuccess: async () => {
|
||||
toast.success("Note deleted");
|
||||
await queryClient.invalidateQueries();
|
||||
},
|
||||
onError: (error) => toast.error(error.message),
|
||||
});
|
||||
|
||||
if (notes.isLoading || servers.isLoading) {
|
||||
return <QueryStateCard title="Loading notes" description="Fetching server documentation and internal notes." />;
|
||||
}
|
||||
|
||||
if (notes.isError || servers.isError || !notes.data || !servers.data) {
|
||||
return <QueryStateCard title="Notes unavailable" description="The note list could not be loaded." />;
|
||||
}
|
||||
|
||||
const serverMap = new Map(servers.data.map((item) => [item.id, item.name]));
|
||||
|
||||
return (
|
||||
<PageSection
|
||||
title="Notes"
|
||||
description="Manage documentation, incident notes, and internal reminders for each server."
|
||||
action={
|
||||
<Link to="/dashboard/notes/new">
|
||||
<Button>New note</Button>
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
<ResourceListCard
|
||||
title="Server notes"
|
||||
description="Each note belongs to one server and acts as lightweight internal documentation."
|
||||
columns={[
|
||||
{
|
||||
key: "title",
|
||||
header: "Title",
|
||||
render: (item: (typeof notes.data)[number]) => (
|
||||
<div className="grid gap-1">
|
||||
<Link to="/dashboard/notes/$noteId" params={{ noteId: item.id }} className="font-medium hover:underline">
|
||||
{item.title}
|
||||
</Link>
|
||||
<span className="text-xs text-muted-foreground line-clamp-2">{item.content}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "server",
|
||||
header: "Server",
|
||||
render: (item: (typeof notes.data)[number]) => serverMap.get(item.serverId) || item.serverId,
|
||||
},
|
||||
{
|
||||
key: "category",
|
||||
header: "Category",
|
||||
render: (item: (typeof notes.data)[number]) => item.category || "-",
|
||||
},
|
||||
{
|
||||
key: "updatedAt",
|
||||
header: "Updated",
|
||||
render: (item: (typeof notes.data)[number]) => new Date(item.updatedAt).toLocaleString(),
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
header: "Actions",
|
||||
className: "w-28",
|
||||
render: (item: (typeof notes.data)[number]) => (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="h-auto px-0 text-red-600 hover:text-red-700"
|
||||
disabled={removeNote.isPending}
|
||||
onClick={() => {
|
||||
if (confirm(`Delete note \"${item.title}\"?`)) {
|
||||
removeNote.mutate(item.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
items={notes.data}
|
||||
emptyTitle="No notes yet"
|
||||
emptyDescription="Add a note to document maintenance, incidents, or general server information."
|
||||
/>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue