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>
|
||
);
|
||
} |