haixunMaster/components/product-profile/brand-product-picker.tsx

176 lines
5.3 KiB
TypeScript
Raw Normal View History

2026-06-21 12:50:31 +00:00
"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>
);
}