Refactor requests, refactor users, add home dashboard
This commit is contained in:
@ -1,13 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Download, ExternalLink } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import type { CommissionStatus } from "@/schemas/commissions/tableSchema";
|
||||
|
||||
import { deleteCommissionRequest } from "@/actions/commissions/requests/deleteCommissionRequest";
|
||||
import { updateCommissionRequest } from "@/actions/commissions/requests/updateCommissionRequest";
|
||||
import {
|
||||
@ -31,6 +23,12 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type { CommissionStatus } from "@/schemas/commissions/requests";
|
||||
import { Download, ExternalLink } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type RequestFile = {
|
||||
id: string;
|
||||
@ -65,6 +63,7 @@ const STATUS_OPTIONS: CommissionStatus[] = [
|
||||
"REVIEWING",
|
||||
"ACCEPTED",
|
||||
"REJECTED",
|
||||
"INPROGRESS",
|
||||
"COMPLETED",
|
||||
"SPAM",
|
||||
];
|
||||
@ -114,30 +113,30 @@ export function CommissionRequestEditor({ request }: { request: RequestShape })
|
||||
message !== request.message;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-8">
|
||||
{/* Top bar */}
|
||||
<div className="flex flex-col gap-3 rounded-2xl border bg-card p-4 shadow-sm md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="secondary" className="px-2 py-0.5">
|
||||
{/* <Badge variant="secondary" className="px-2 py-0.5">
|
||||
{request.files.length} file{request.files.length === 1 ? "" : "s"}
|
||||
</Badge>
|
||||
</Badge> */}
|
||||
<Badge variant="secondary" className="px-2 py-0.5">
|
||||
Status: {request.status}
|
||||
</Badge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{/* <span className="text-sm text-muted-foreground">
|
||||
Updated: {new Date(request.updatedAt).toLocaleString()}
|
||||
</span>
|
||||
</span> */}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{request.files.length > 1 ? (
|
||||
{/* {request.files.length > 1 ? (
|
||||
<Button asChild variant="outline" className="h-9">
|
||||
<a href={bulkUrl(request.id)}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download all (ZIP)
|
||||
</a>
|
||||
</Button>
|
||||
) : null}
|
||||
) : null} */}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
@ -174,21 +173,21 @@ export function CommissionRequestEditor({ request }: { request: RequestShape })
|
||||
{isSaving ? "Saving…" : "Save changes"}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
{/* <Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
disabled={isSaving}
|
||||
onClick={() => setDeleteOpen(true)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Button> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Request details (artist-facing) */}
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
<div className="space-y-4 rounded-2xl border bg-card p-4 shadow-sm lg:col-span-1">
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-4 border bg-card p-4 shadow-sm lg:col-span-1">
|
||||
<div className="space-y-2 border-b pb-4">
|
||||
<div className="text-sm font-semibold">Status</div>
|
||||
<Select value={status} onValueChange={(v) => setStatus(v as CommissionStatus)}>
|
||||
<SelectTrigger className="h-9">
|
||||
@ -208,6 +207,17 @@ export function CommissionRequestEditor({ request }: { request: RequestShape })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 border-b pb-4">
|
||||
<div className="text-sm font-semibold">Customer</div>
|
||||
<Input value={customerName} onChange={(e) => setCustomerName(e.target.value)} />
|
||||
<Input value={customerEmail} onChange={(e) => setCustomerEmail(e.target.value)} />
|
||||
<Input
|
||||
placeholder="Socials (optional)"
|
||||
value={customerSocials}
|
||||
onChange={(e) => setCustomerSocials(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-semibold">Selection</div>
|
||||
|
||||
@ -224,14 +234,14 @@ export function CommissionRequestEditor({ request }: { request: RequestShape })
|
||||
<div className="text-sm">
|
||||
<div className="text-muted-foreground text-xs">Extras</div>
|
||||
{request.extras?.length ? (
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
<div className="mt-1 flex flex-wrap flex-col gap-2">
|
||||
{request.extras.map((e) => (
|
||||
<span
|
||||
<div
|
||||
key={e.id}
|
||||
className="inline-flex items-center rounded-md border bg-muted/40 px-2 py-0.5 text-xs text-foreground/80"
|
||||
>
|
||||
{e.name}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
@ -244,28 +254,28 @@ export function CommissionRequestEditor({ request }: { request: RequestShape })
|
||||
<div className="font-medium tabular-nums">
|
||||
{request.priceEstimate
|
||||
? request.priceEstimate.min === request.priceEstimate.max
|
||||
? `€${request.priceEstimate.min.toFixed(2)}`
|
||||
: `€${request.priceEstimate.min.toFixed(2)} – €${request.priceEstimate.max.toFixed(2)}`
|
||||
? `€ ${request.priceEstimate.min.toFixed(2)}`
|
||||
: `€ ${request.priceEstimate.min.toFixed(2)} – € ${request.priceEstimate.max.toFixed(2)}`
|
||||
: "—"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-semibold">Customer</div>
|
||||
<Input value={customerName} onChange={(e) => setCustomerName(e.target.value)} />
|
||||
<Input value={customerEmail} onChange={(e) => setCustomerEmail(e.target.value)} />
|
||||
<Input
|
||||
placeholder="Socials (optional)"
|
||||
value={customerSocials}
|
||||
onChange={(e) => setCustomerSocials(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
{/* Message */}
|
||||
<div className="space-y-2 border bg-card p-4 shadow-sm">
|
||||
<div className="text-sm font-semibold">Message</div>
|
||||
<Textarea
|
||||
rows={10}
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
className="leading-relaxed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Images / files */}
|
||||
<div className="space-y-3 rounded-2xl border bg-card p-4 shadow-sm">
|
||||
<div className="space-y-2 border bg-card p-4 shadow-sm">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-sm font-semibold">Reference Images</div>
|
||||
{request.files.length > 1 ? (
|
||||
@ -339,17 +349,6 @@ export function CommissionRequestEditor({ request }: { request: RequestShape })
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
<div className="space-y-2 rounded-2xl border bg-card p-4 shadow-sm">
|
||||
<div className="text-sm font-semibold">Message</div>
|
||||
<Textarea
|
||||
rows={10}
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
className="leading-relaxed"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,32 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
SortingState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
ArrowUpDown,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import * as React from "react";
|
||||
|
||||
import { deleteCommissionRequest } from "@/actions/commissions/requests/deleteCommissionRequest";
|
||||
import { getCommissionRequestsTablePage } from "@/actions/commissions/requests/getCommissionRequestsTablePage";
|
||||
import { setCommissionRequestStatus } from "@/actions/commissions/requests/setCommissionRequestStatus";
|
||||
import type {
|
||||
CommissionRequestTableRow,
|
||||
CommissionStatus,
|
||||
} from "@/schemas/commissions/tableSchema";
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@ -62,21 +38,30 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { CommissionRequestTableRow, CommissionStatus } from "@/schemas/commissions/requests";
|
||||
import {
|
||||
ColumnDef,
|
||||
SortingState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
ArrowUpDown,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import * as React from "react";
|
||||
|
||||
type TriState = "any" | "true" | "false";
|
||||
|
||||
function useDebouncedValue<T>(value: T, delayMs: number) {
|
||||
const [debounced, setDebounced] = React.useState(value);
|
||||
React.useEffect(() => {
|
||||
const t = setTimeout(() => setDebounced(value), delayMs);
|
||||
return () => clearTimeout(t);
|
||||
}, [value, delayMs]);
|
||||
return debounced;
|
||||
}
|
||||
|
||||
function SortHeader(props: { title: string; column: any }) {
|
||||
const sorted = props.column.getIsSorted() as false | "asc" | "desc";
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@ -110,6 +95,15 @@ function StatusBadge({ status }: { status: CommissionStatus }) {
|
||||
);
|
||||
}
|
||||
|
||||
function useDebouncedValue<T>(value: T, delayMs: number) {
|
||||
const [debounced, setDebounced] = React.useState(value);
|
||||
React.useEffect(() => {
|
||||
const t = setTimeout(() => setDebounced(value), delayMs);
|
||||
return () => clearTimeout(t);
|
||||
}, [value, delayMs]);
|
||||
return debounced;
|
||||
}
|
||||
|
||||
function TriSelectInline(props: { value: TriState; onChange: (v: TriState) => void }) {
|
||||
return (
|
||||
<Select value={props.value} onValueChange={(v) => props.onChange(v as TriState)}>
|
||||
@ -130,6 +124,7 @@ const STATUS_OPTIONS: CommissionStatus[] = [
|
||||
"REVIEWING",
|
||||
"ACCEPTED",
|
||||
"REJECTED",
|
||||
"INPROGRESS",
|
||||
"COMPLETED",
|
||||
"SPAM",
|
||||
];
|
||||
@ -202,190 +197,153 @@ export function CommissionRequestsTable() {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
// React.useEffect(() => {
|
||||
// // Only poll when tab is visible (avoid wasting requests)
|
||||
// let timer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
// const start = () => {
|
||||
// if (timer) return;
|
||||
// timer = setInterval(() => {
|
||||
// // Avoid stacking requests
|
||||
// if (!isPending) refresh();
|
||||
// }, 10_000); // every 10s (adjust as you like)
|
||||
// };
|
||||
|
||||
// const stop = () => {
|
||||
// if (!timer) return;
|
||||
// clearInterval(timer);
|
||||
// timer = null;
|
||||
// };
|
||||
|
||||
// const onVisibilityChange = () => {
|
||||
// if (document.visibilityState === "visible") {
|
||||
// // Do an immediate refresh when the user returns
|
||||
// refresh();
|
||||
// start();
|
||||
// } else {
|
||||
// stop();
|
||||
// }
|
||||
// };
|
||||
|
||||
// onVisibilityChange(); // initialize
|
||||
|
||||
// document.addEventListener("visibilitychange", onVisibilityChange);
|
||||
// return () => {
|
||||
// stop();
|
||||
// document.removeEventListener("visibilitychange", onVisibilityChange);
|
||||
// };
|
||||
// }, [refresh, isPending]);
|
||||
|
||||
// React.useEffect(() => {
|
||||
// const onFocus = () => refresh();
|
||||
// window.addEventListener("focus", onFocus);
|
||||
// return () => window.removeEventListener("focus", onFocus);
|
||||
// }, [refresh]);
|
||||
|
||||
const columns = React.useMemo<ColumnDef<CommissionRequestTableRow>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: ({ column }) => <SortHeader title="Submitted" column={column} />,
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm text-foreground/80">
|
||||
{new Date(row.original.createdAt).toLocaleString()}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "filesCount",
|
||||
header: ({ column }) => <SortHeader title="Files" column={column} />,
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm font-medium tabular-nums">{row.original.filesCount}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "requestor",
|
||||
header: "Requestor",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return (
|
||||
<div className="min-w-0 max-w-[420px]">
|
||||
<div className="truncate text-sm font-medium">{r.customerName}</div>
|
||||
<div className="truncate text-[11px] leading-4 text-muted-foreground">
|
||||
{r.customerEmail}
|
||||
{r.customerSocials ? ` · ${r.customerSocials}` : ""}
|
||||
</div>
|
||||
const columns = React.useMemo<ColumnDef<CommissionRequestTableRow>[]>(() => [
|
||||
{
|
||||
accessorKey: "index",
|
||||
header: "Index",
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm font-medium tabular-nums">#{row.index + 1}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "requestor",
|
||||
header: "Requestor",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return (
|
||||
<div className="min-w-0 max-w-105">
|
||||
<div className="truncate text-sm font-medium">{r.customerName}</div>
|
||||
<div className="truncate text-[11px] leading-4 text-muted-foreground">
|
||||
{r.customerEmail}
|
||||
{r.customerSocials ? ` · ${r.customerSocials}` : ""}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: ({ column }) => <SortHeader title="Status" column={column} />,
|
||||
cell: ({ row }) => <StatusBadge status={row.original.status} />,
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: ({ column }) => <SortHeader title="Submitted" column={column} />,
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm text-foreground/80">
|
||||
{new Date(row.original.createdAt).toLocaleString()}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "filesCount",
|
||||
header: ({ column }) => <SortHeader title="Files" column={column} />,
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm font-medium tabular-nums">{row.original.fileCount}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: ({ column }) => <SortHeader title="Status" column={column} />,
|
||||
cell: ({ row }) => <StatusBadge status={row.original.status} />,
|
||||
},
|
||||
{
|
||||
id: "complete",
|
||||
header: "",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
const isCompleted = r.status === "COMPLETED";
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant={isCompleted ? "secondary" : "outline"}
|
||||
size="icon"
|
||||
className="h-9 w-9"
|
||||
disabled={isPending || isCompleted}
|
||||
title={isCompleted ? "Completed" : "Mark as completed"}
|
||||
onClick={() => {
|
||||
if (isCompleted) return;
|
||||
startTransition(async () => {
|
||||
await setCommissionRequestStatus({ id: r.id, status: "COMPLETED" });
|
||||
refresh();
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
<span className="sr-only">Mark complete</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{
|
||||
id: "complete",
|
||||
header: "",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
const isCompleted = r.status === "COMPLETED";
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant={isCompleted ? "secondary" : "outline"}
|
||||
size="icon"
|
||||
className="h-9 w-9"
|
||||
disabled={isPending || isCompleted}
|
||||
title={isCompleted ? "Completed" : "Mark as completed"}
|
||||
onClick={() => {
|
||||
if (isCompleted) return;
|
||||
startTransition(async () => {
|
||||
await setCommissionRequestStatus({ id: r.id, status: "COMPLETED" });
|
||||
refresh();
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
<span className="sr-only">Mark complete</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-9 w-9">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">Open row actions</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
{/* status changes */}
|
||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
||||
Set status
|
||||
</div>
|
||||
{STATUS_OPTIONS.filter((s) => s !== "COMPLETED").map((s) => (
|
||||
<DropdownMenuItem
|
||||
key={s}
|
||||
className="cursor-pointer"
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
startTransition(async () => {
|
||||
await setCommissionRequestStatus({ id: r.id, status: s });
|
||||
refresh();
|
||||
});
|
||||
}}
|
||||
>
|
||||
{s}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/commissions/requests/${r.id}`} className="cursor-pointer">
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-9 w-9">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">Open row actions</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
{/* status changes */}
|
||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
||||
Set status
|
||||
</div>
|
||||
{STATUS_OPTIONS.filter((s) => s !== "COMPLETED").map((s) => (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer text-destructive focus:text-destructive"
|
||||
key={s}
|
||||
className="cursor-pointer"
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
setDeleteTarget({
|
||||
id: r.id,
|
||||
label: `${r.customerName} (${r.customerEmail})`,
|
||||
startTransition(async () => {
|
||||
await setCommissionRequestStatus({ id: r.id, status: s });
|
||||
refresh();
|
||||
});
|
||||
setDeleteOpen(true);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
{s}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
))}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/commissions/requests/${r.id}`} className="cursor-pointer">
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer text-destructive focus:text-destructive"
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
setDeleteTarget({
|
||||
id: r.id,
|
||||
label: `${r.customerName} (${r.customerEmail})`,
|
||||
});
|
||||
setDeleteOpen(true);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[isPending, refresh],
|
||||
);
|
||||
|
||||
@ -409,7 +367,7 @@ export function CommissionRequestsTable() {
|
||||
<div className="space-y-4">
|
||||
<div className="overflow-hidden rounded-2xl border border-border/60 bg-card shadow-sm ring-1 ring-border/40">
|
||||
<div className="relative">
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 z-10 h-6 bg-gradient-to-b from-background/60 to-transparent" />
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 z-10 h-6 bg-linear-to-b from-background/60 to-transparent" />
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0 z-20 bg-card">
|
||||
<TableRow className="hover:bg-transparent">
|
||||
@ -541,7 +499,7 @@ export function CommissionRequestsTable() {
|
||||
setPageIndex(0);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-9 w-[120px]">
|
||||
<SelectTrigger className="h-9 w-30">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@ -570,7 +528,7 @@ export function CommissionRequestsTable() {
|
||||
Prev
|
||||
</Button>
|
||||
|
||||
<div className="min-w-[120px] text-center text-sm tabular-nums">
|
||||
<div className="min-w-30 text-center text-sm tabular-nums">
|
||||
Page {pageIndex + 1} / {pageCount}
|
||||
</div>
|
||||
|
||||
78
src/components/commissions/requests/RequestsTable.tsx
Normal file
78
src/components/commissions/requests/RequestsTable.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
"use client"
|
||||
|
||||
import { deleteCommissionRequest } from "@/actions/commissions/requests/deleteCommissionRequest";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { CommissionRequest } from "@/generated/prisma/client";
|
||||
import { PencilIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
type CommissionRequestWithCount = CommissionRequest & {
|
||||
_count: {
|
||||
files: number;
|
||||
};
|
||||
};
|
||||
|
||||
export default function RequestsTable({ requests }: { requests: CommissionRequestWithCount[] }) {
|
||||
const handleDelete = (id: string) => {
|
||||
deleteCommissionRequest(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[8%]">Index</TableHead>
|
||||
<TableHead className="w-[68%]">Requester</TableHead>
|
||||
<TableHead className="w-[8%]">Status</TableHead>
|
||||
<TableHead className="w-[8%]">Files</TableHead>
|
||||
<TableHead className="w-[8%] text-right" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{requests.map((r) => (
|
||||
<TableRow key={r.id}>
|
||||
<TableCell className="font-medium">
|
||||
<Link href={`/commissions/requests/${r.id}`}># {r.index}</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
{r.customerName}
|
||||
</div>
|
||||
<div className="font-mono text-sm text-muted-foreground">{r.customerEmail} / {r.customerSocials ? r.customerSocials : "-"} </div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<span className="rounded bg-muted px-2 py-1 text-xs">
|
||||
{r.status.toLowerCase()}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="tabular-nums">
|
||||
{r._count.files}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Link href={`/commissions/requests/${r.id}`} aria-label={`Edit ${r.index}`}>
|
||||
<Button size="icon" variant="secondary">
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{/* <Button
|
||||
size="icon"
|
||||
variant="destructive"
|
||||
aria-label={`Delete ${r.index}`}
|
||||
onClick={() => handleDelete(r.id)}
|
||||
>
|
||||
<Trash2Icon className="h-4 w-4" />
|
||||
</Button> */}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@ -28,7 +28,7 @@ const artworkItems = [
|
||||
const commissionItems = [
|
||||
{
|
||||
title: "Requests",
|
||||
href: "/commissions",
|
||||
href: "/commissions/requests",
|
||||
},
|
||||
{
|
||||
title: "Types",
|
||||
|
||||
35
src/components/home/StatCard.tsx
Normal file
35
src/components/home/StatCard.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import Link from "next/link";
|
||||
import * as React from "react";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
export function StatCard(props: {
|
||||
title: string;
|
||||
value: React.ReactNode;
|
||||
hint?: React.ReactNode;
|
||||
href?: string;
|
||||
}) {
|
||||
const inner = (
|
||||
<Card className="h-full">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
{props.title}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-semibold">{props.value}</div>
|
||||
{props.hint ? (
|
||||
<div className="mt-1 text-sm text-muted-foreground">{props.hint}</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return props.href ? (
|
||||
<Link href={props.href} className="block h-full">
|
||||
{inner}
|
||||
</Link>
|
||||
) : (
|
||||
inner
|
||||
);
|
||||
}
|
||||
8
src/components/home/StatusPill.tsx
Normal file
8
src/components/home/StatusPill.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
export function StatusPill(props: { label: string; value: number }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between rounded-md border px-3 py-2 text-sm">
|
||||
<span className="text-muted-foreground">{props.label}</span>
|
||||
<span className="font-medium tabular-nums">{props.value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user