330 lines
8.8 KiB
TypeScript
330 lines
8.8 KiB
TypeScript
"use client";
|
|
|
|
import { Loader2, Package, Pencil, Plus, Trash2 } from "lucide-react";
|
|
import { useCallback, useEffect, useState } from "react";
|
|
import { createBcItem, deleteBcItem, updateBcItem } from "@/actions/catalog-bc";
|
|
import { useConfirmDialog } from "@/components/admin/confirm-dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import { useLatestRequest } from "@/features/catalog/client/use-latest-request";
|
|
import { useServerAction } from "@/hooks/use-server-action";
|
|
|
|
interface BcItem {
|
|
id: number;
|
|
pageId: number;
|
|
itemIds: string;
|
|
catalogName: string;
|
|
orderNumber: number;
|
|
extradata: string;
|
|
}
|
|
|
|
interface BcItemsEditorProps {
|
|
pageId: number;
|
|
canEdit: boolean;
|
|
}
|
|
|
|
export function BcItemsEditor({ pageId, canEdit }: BcItemsEditorProps) {
|
|
const { confirm, dialog: confirmDialog } = useConfirmDialog();
|
|
const requests = useLatestRequest();
|
|
const [items, setItems] = useState<BcItem[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState(false);
|
|
const [editingItem, setEditingItem] = useState<BcItem | null>(null);
|
|
const [addingItem, setAddingItem] = useState(false);
|
|
|
|
const load = useCallback(async () => {
|
|
const request = requests.start();
|
|
setError(false);
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch(
|
|
`/api/admin/catalog/items?pageId=${pageId}&catalog=bc`,
|
|
{ signal: request.signal },
|
|
);
|
|
if (!res.ok) throw new Error();
|
|
const data = await res.json();
|
|
if (!request.isCurrent()) return;
|
|
setItems(Array.isArray(data.items) ? data.items : []);
|
|
} catch {
|
|
if (!request.isCurrent()) return;
|
|
setError(true);
|
|
} finally {
|
|
if (request.isCurrent()) setLoading(false);
|
|
}
|
|
}, [pageId, requests]);
|
|
|
|
useEffect(() => {
|
|
if (pageId > 0) void load();
|
|
return () => requests.cancel();
|
|
}, [pageId, load, requests]);
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{confirmDialog}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2 text-sm font-medium">
|
|
<Package className="h-4 w-4 text-muted-foreground" />
|
|
BC Items ({items.length})
|
|
</div>
|
|
{canEdit && (
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-7 text-xs"
|
|
onClick={() => setAddingItem(true)}
|
|
>
|
|
<Plus className="h-3 w-3 mr-1" />
|
|
Add Item
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
</div>
|
|
) : error ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Button variant="outline" size="sm" onClick={() => void load()}>
|
|
Retry
|
|
</Button>
|
|
</div>
|
|
) : items.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground text-center py-8 rounded-md border border-dashed">
|
|
No items in this page.
|
|
</p>
|
|
) : (
|
|
<div className="rounded-md border overflow-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="text-xs w-16">ID</TableHead>
|
|
<TableHead className="text-xs">Name</TableHead>
|
|
<TableHead className="text-xs">Item IDs</TableHead>
|
|
<TableHead className="text-xs w-20">Order</TableHead>
|
|
<TableHead className="text-xs">Extra Data</TableHead>
|
|
{canEdit && (
|
|
<TableHead className="text-xs w-20">Actions</TableHead>
|
|
)}
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{items.map((item) => (
|
|
<TableRow key={item.id}>
|
|
<TableCell className="text-xs font-mono">{item.id}</TableCell>
|
|
<TableCell className="text-xs font-medium">
|
|
{item.catalogName}
|
|
</TableCell>
|
|
<TableCell className="text-xs font-mono text-muted-foreground">
|
|
{item.itemIds}
|
|
</TableCell>
|
|
<TableCell className="text-xs">{item.orderNumber}</TableCell>
|
|
<TableCell className="text-xs text-muted-foreground max-w-32 truncate">
|
|
{item.extradata || "\u2014"}
|
|
</TableCell>
|
|
{canEdit && (
|
|
<TableCell>
|
|
<div className="flex gap-1">
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 w-6 p-0"
|
|
onClick={() => setEditingItem(item)}
|
|
>
|
|
<Pencil className="h-3 w-3" />
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 w-6 p-0 text-[var(--admin-error)] hover:text-[var(--admin-error)]"
|
|
onClick={async () => {
|
|
const ok = await confirm({
|
|
title: "Delete BC item",
|
|
description: `Delete item "${item.catalogName}"?`,
|
|
confirmLabel: "Delete",
|
|
});
|
|
if (!ok) return;
|
|
const result = await deleteBcItem({
|
|
id: item.id,
|
|
});
|
|
if (result.ok) {
|
|
setItems((prev) =>
|
|
prev.filter((i) => i.id !== item.id),
|
|
);
|
|
}
|
|
}}
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
)}
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
)}
|
|
|
|
{editingItem && (
|
|
<BcItemDialog
|
|
item={editingItem}
|
|
mode="edit"
|
|
onClose={() => setEditingItem(null)}
|
|
onSubmit={async (data) => {
|
|
const result = await updateBcItem({ id: editingItem.id, ...data });
|
|
if (result.ok) {
|
|
setItems((prev) =>
|
|
prev.map((i) =>
|
|
i.id === editingItem.id ? { ...i, ...data } : i,
|
|
),
|
|
);
|
|
}
|
|
return result;
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{addingItem && (
|
|
<BcItemDialog
|
|
item={{
|
|
id: 0,
|
|
pageId,
|
|
itemIds: "",
|
|
catalogName: "",
|
|
orderNumber: 1,
|
|
extradata: "",
|
|
}}
|
|
mode="add"
|
|
onClose={() => setAddingItem(false)}
|
|
onSubmit={async (data) => {
|
|
const result = await createBcItem({ pageId, ...data });
|
|
if (result.ok) await load();
|
|
return result;
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function BcItemDialog({
|
|
item,
|
|
mode,
|
|
onClose,
|
|
onSubmit,
|
|
}: {
|
|
item: BcItem;
|
|
mode: "add" | "edit";
|
|
onClose: () => void;
|
|
onSubmit: (data: {
|
|
itemIds: string;
|
|
catalogName: string;
|
|
orderNumber: number;
|
|
extradata: string;
|
|
}) => Promise<{ ok: boolean; error?: string }>;
|
|
}) {
|
|
const { run, isPending } = useServerAction();
|
|
const [form, setForm] = useState({
|
|
itemIds: item.itemIds,
|
|
catalogName: item.catalogName,
|
|
orderNumber: item.orderNumber,
|
|
extradata: item.extradata,
|
|
});
|
|
|
|
return (
|
|
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{mode === "add" ? "Add BC Item" : `Edit Item #${item.id}`}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-2">
|
|
<div className="space-y-2">
|
|
<Label>Catalog Name</Label>
|
|
<Input
|
|
value={form.catalogName}
|
|
onChange={(e) =>
|
|
setForm((f) => ({ ...f, catalogName: e.target.value }))
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Item IDs (semicolon-separated)</Label>
|
|
<Input
|
|
value={form.itemIds}
|
|
onChange={(e) =>
|
|
setForm((f) => ({ ...f, itemIds: e.target.value }))
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>Order</Label>
|
|
<Input
|
|
type="number"
|
|
value={form.orderNumber}
|
|
onChange={(e) =>
|
|
setForm((f) => ({
|
|
...f,
|
|
orderNumber: parseInt(e.target.value, 10) || 0,
|
|
}))
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Extra Data</Label>
|
|
<Input
|
|
value={form.extradata}
|
|
onChange={(e) =>
|
|
setForm((f) => ({ ...f, extradata: e.target.value }))
|
|
}
|
|
placeholder="Optional"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={onClose} disabled={isPending}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={() => {
|
|
run(() => onSubmit(form), {
|
|
successMessage:
|
|
mode === "add" ? "Item created." : "Item updated.",
|
|
errorMessage: "Failed to save.",
|
|
onSuccess: () => onClose(),
|
|
});
|
|
}}
|
|
disabled={isPending || !form.catalogName || !form.itemIds}
|
|
>
|
|
{isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
{mode === "add" ? "Add Item" : "Save"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|