176 lines
5.3 KiB
TypeScript
176 lines
5.3 KiB
TypeScript
|
|
"use client";
|
|||
|
|
|
|||
|
|
import Link from "next/link";
|
|||
|
|
import { useEffect, useMemo, useState } from "react";
|
|||
|
|
import { Plus, Settings2 } from "lucide-react";
|
|||
|
|
import { BrandFormDialog } from "@/components/product-profile/brand-form-dialog";
|
|||
|
|
import { Button } from "@/components/ui/button";
|
|||
|
|
import { Label } from "@/components/ui/label";
|
|||
|
|
import {
|
|||
|
|
Select,
|
|||
|
|
SelectContent,
|
|||
|
|
SelectItem,
|
|||
|
|
SelectTrigger,
|
|||
|
|
SelectValue,
|
|||
|
|
} from "@/components/ui/select";
|
|||
|
|
import {
|
|||
|
|
formatCatalogProductSummary,
|
|||
|
|
type CatalogBrand,
|
|||
|
|
} from "@/lib/types/product-catalog";
|
|||
|
|
|
|||
|
|
interface BrandProductPickerProps {
|
|||
|
|
brandId: string | null;
|
|||
|
|
productId: string | null;
|
|||
|
|
onChange: (selection: {
|
|||
|
|
brandId: string | null;
|
|||
|
|
productId: string | null;
|
|||
|
|
context?: string;
|
|||
|
|
}) => void;
|
|||
|
|
label?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function BrandProductPicker({
|
|||
|
|
brandId,
|
|||
|
|
productId,
|
|||
|
|
onChange,
|
|||
|
|
label = "品牌與產品",
|
|||
|
|
}: BrandProductPickerProps) {
|
|||
|
|
const [brands, setBrands] = useState<CatalogBrand[]>([]);
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
const [createBrandOpen, setCreateBrandOpen] = useState(false);
|
|||
|
|
|
|||
|
|
async function load() {
|
|||
|
|
setLoading(true);
|
|||
|
|
try {
|
|||
|
|
const res = await fetch("/api/brands?all=1&productLimit=200");
|
|||
|
|
const data = await res.json().catch(() => ({}));
|
|||
|
|
setBrands(data.brands ?? []);
|
|||
|
|
} catch {
|
|||
|
|
// 網路異常時保持空列表
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
load();
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
const selectedBrand = useMemo(
|
|||
|
|
() => brands.find((b) => b.id === brandId) ?? null,
|
|||
|
|
[brands, brandId]
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const selectedProduct = useMemo(() => {
|
|||
|
|
if (!selectedBrand || !productId) return null;
|
|||
|
|
return selectedBrand.products.find((p) => p.id === productId) ?? null;
|
|||
|
|
}, [selectedBrand, productId]);
|
|||
|
|
|
|||
|
|
const preview =
|
|||
|
|
selectedBrand && selectedProduct
|
|||
|
|
? formatCatalogProductSummary(selectedBrand.name, selectedProduct.context, selectedProduct.label)
|
|||
|
|
: selectedBrand
|
|||
|
|
? `${selectedBrand.name} · ${selectedBrand.products.length} 個產品(生成時依標籤自動推薦)`
|
|||
|
|
: null;
|
|||
|
|
|
|||
|
|
function emit(nextBrandId: string | null, nextProductId: string | null) {
|
|||
|
|
if (!nextBrandId) {
|
|||
|
|
onChange({ brandId: null, productId: null });
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const brand = brands.find((b) => b.id === nextBrandId);
|
|||
|
|
if (!brand) return;
|
|||
|
|
|
|||
|
|
const product =
|
|||
|
|
nextProductId && nextProductId !== "__auto__"
|
|||
|
|
? brand.products.find((p) => p.id === nextProductId) ?? null
|
|||
|
|
: null;
|
|||
|
|
|
|||
|
|
const context = product
|
|||
|
|
? formatCatalogProductSummary(brand.name, product.context, product.label)
|
|||
|
|
: undefined;
|
|||
|
|
|
|||
|
|
onChange({
|
|||
|
|
brandId: nextBrandId,
|
|||
|
|
productId: nextProductId === "__auto__" ? null : nextProductId,
|
|||
|
|
context,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
<div className="flex items-center justify-between gap-2">
|
|||
|
|
<Label>{label}</Label>
|
|||
|
|
<Link
|
|||
|
|
href="/products"
|
|||
|
|
className="inline-flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground"
|
|||
|
|
>
|
|||
|
|
<Settings2 className="h-3 w-3" />
|
|||
|
|
管理品牌與產品
|
|||
|
|
</Link>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex flex-wrap gap-2">
|
|||
|
|
<Select
|
|||
|
|
value={brandId ?? "__none__"}
|
|||
|
|
onValueChange={(next) => {
|
|||
|
|
if (next === "__none__") emit(null, null);
|
|||
|
|
else emit(next, "__auto__");
|
|||
|
|
}}
|
|||
|
|
disabled={loading}
|
|||
|
|
>
|
|||
|
|
<SelectTrigger className="min-w-[10rem] flex-1">
|
|||
|
|
<SelectValue placeholder={loading ? "載入中…" : "選擇品牌"} />
|
|||
|
|
</SelectTrigger>
|
|||
|
|
<SelectContent>
|
|||
|
|
<SelectItem value="__none__">(尚未選擇)</SelectItem>
|
|||
|
|
{brands.map((brand) => (
|
|||
|
|
<SelectItem key={brand.id} value={brand.id}>
|
|||
|
|
{brand.name}({brand.products.length} 個產品)
|
|||
|
|
</SelectItem>
|
|||
|
|
))}
|
|||
|
|
</SelectContent>
|
|||
|
|
</Select>
|
|||
|
|
|
|||
|
|
<Button type="button" size="sm" variant="outline" onClick={() => setCreateBrandOpen(true)}>
|
|||
|
|
<Plus className="h-3.5 w-3.5" />
|
|||
|
|
新建品牌
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{selectedBrand && selectedBrand.products.length > 0 && (
|
|||
|
|
<Select
|
|||
|
|
value={productId ?? "__auto__"}
|
|||
|
|
onValueChange={(next) => emit(selectedBrand.id, next)}
|
|||
|
|
>
|
|||
|
|
<SelectTrigger className="w-full">
|
|||
|
|
<SelectValue />
|
|||
|
|
</SelectTrigger>
|
|||
|
|
<SelectContent>
|
|||
|
|
<SelectItem value="__auto__">自動依海巡標籤推薦</SelectItem>
|
|||
|
|
{selectedBrand.products.map((product) => (
|
|||
|
|
<SelectItem key={product.id} value={product.id}>
|
|||
|
|
{product.label}
|
|||
|
|
{product.matchTags.length > 0 && ` · ${product.matchTags.slice(0, 3).join("、")}`}
|
|||
|
|
</SelectItem>
|
|||
|
|
))}
|
|||
|
|
</SelectContent>
|
|||
|
|
</Select>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{selectedBrand && selectedBrand.products.length === 0 && (
|
|||
|
|
<p className="text-[12px] text-muted-foreground">
|
|||
|
|
此品牌尚無產品,請到「品牌與產品」頁新增。
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{preview && <p className="text-[12px] text-muted-foreground">{preview}</p>}
|
|||
|
|
|
|||
|
|
<BrandFormDialog
|
|||
|
|
open={createBrandOpen}
|
|||
|
|
onOpenChange={setCreateBrandOpen}
|
|||
|
|
onSaved={() => load()}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|