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

176 lines
5.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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