147 lines
4.6 KiB
TypeScript
147 lines
4.6 KiB
TypeScript
import { useEffect, useState } from "react";
|
||
import { SERVICE_AREAS } from "./serviceAreas";
|
||
import { Button, Textarea } from "../ui";
|
||
import { useRepos } from "../../data/DataContext";
|
||
import type { Brand, BrandProduct } from "../../domain/types";
|
||
import { useI18n } from "../../i18n/I18nContext";
|
||
import { useFormatApiError } from "../../lib/apiErrors";
|
||
import { expandIncludeTerms } from "../../lib/threadsTerm";
|
||
import { ProductWatchForm } from "./ProductWatchForm";
|
||
|
||
function splitTerms(raw: string): string[] {
|
||
return raw
|
||
.split(/[\n,、]/)
|
||
.map((s) => s.trim())
|
||
.filter(Boolean);
|
||
}
|
||
|
||
type Props = {
|
||
/** firstSweepTriggered:後端已排第一輪,前端不該再叫使用者按「立即巡邏」。 */
|
||
onCreated: (result: { firstSweepTriggered: boolean }) => void;
|
||
};
|
||
|
||
/** 今日空狀態:關鍵字就能開工,品牌/產品是選項。 */
|
||
export function QuickWatchStart({ onCreated }: Props) {
|
||
const { t } = useI18n();
|
||
const repos = useRepos();
|
||
const formatError = useFormatApiError();
|
||
const [terms, setTerms] = useState("");
|
||
const [regions, setRegions] = useState<string[]>([]);
|
||
const [brandId, setBrandId] = useState("");
|
||
const [productId, setProductId] = useState("");
|
||
const [brands, setBrands] = useState<Brand[]>([]);
|
||
const [products, setProducts] = useState<BrandProduct[]>([]);
|
||
const [busy, setBusy] = useState(false);
|
||
const [error, setError] = useState("");
|
||
const scout = (repos as { scout?: { listBrands: () => Promise<Brand[]>; listProducts: (id: string) => Promise<BrandProduct[]> } }).scout;
|
||
|
||
useEffect(() => {
|
||
if (!scout) return;
|
||
void scout.listBrands().then(setBrands).catch(() => setBrands([]));
|
||
}, [scout]);
|
||
|
||
useEffect(() => {
|
||
if (!scout || !brandId) {
|
||
setProducts([]);
|
||
return;
|
||
}
|
||
void scout.listProducts(brandId).then(setProducts).catch(() => setProducts([]));
|
||
}, [scout, brandId]);
|
||
|
||
async function submit() {
|
||
const nextTerms = expandIncludeTerms(splitTerms(terms));
|
||
if (!nextTerms.length) {
|
||
setError(t("radar.watches.threadsRequired"));
|
||
return;
|
||
}
|
||
if ((brandId && !productId) || (!brandId && productId)) {
|
||
setError(t("radar.watches.needBrandProductShort"));
|
||
return;
|
||
}
|
||
setBusy(true);
|
||
setError("");
|
||
try {
|
||
const created = await repos.radar.createWatch({
|
||
terms: nextTerms,
|
||
regions,
|
||
enabled: true,
|
||
brand_id: brandId || undefined,
|
||
product_id: productId || undefined,
|
||
});
|
||
onCreated({ firstSweepTriggered: Boolean(created.first_sweep_triggered) });
|
||
} catch (e) {
|
||
setError(formatError(e));
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<form
|
||
className="hb-radar-form hb-stack"
|
||
onSubmit={(e) => {
|
||
e.preventDefault();
|
||
void submit();
|
||
}}
|
||
>
|
||
<p className="hb-radar-section__hint">{t("radar.start.hint")}</p>
|
||
<Textarea
|
||
name="radar-start-terms"
|
||
label={t("radar.start.terms")}
|
||
hint={t("radar.watches.termsHint")}
|
||
rows={3}
|
||
required
|
||
value={terms}
|
||
onChange={(e) => setTerms(e.target.value)}
|
||
placeholder={t("radar.start.termsPh")}
|
||
/>
|
||
{brands.length ? (
|
||
<ProductWatchForm
|
||
brands={brands}
|
||
products={products}
|
||
brandId={brandId}
|
||
productId={productId}
|
||
onBrandChange={(id) => {
|
||
setBrandId(id);
|
||
setProductId("");
|
||
}}
|
||
onProductChange={(id) => {
|
||
setProductId(id);
|
||
const product = products.find((p) => p.id === id);
|
||
if (!product || terms.trim()) return;
|
||
const seeded = [...product.pain_points, ...product.match_tags].filter(Boolean);
|
||
if (seeded.length) setTerms(seeded.join("\n"));
|
||
}}
|
||
/>
|
||
) : null}
|
||
<p className="hb-radar-section__hint">{t("radar.start.regions")}</p>
|
||
<div className="hb-radar-area-grid">
|
||
{SERVICE_AREAS.map((area) => {
|
||
const on = regions.includes(area.code);
|
||
return (
|
||
<button
|
||
key={area.code}
|
||
type="button"
|
||
aria-pressed={on}
|
||
className={`hb-radar-chip${on ? " is-active" : ""}`}
|
||
onClick={() =>
|
||
setRegions((cur) => (on ? cur.filter((c) => c !== area.code) : [...cur, area.code]))
|
||
}
|
||
>
|
||
{area.label}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
{error ? (
|
||
<p className="hb-form-error" role="alert">
|
||
{error}
|
||
</p>
|
||
) : null}
|
||
<Button type="submit" disabled={busy}>
|
||
{busy ? t("radar.start.saving") : t("radar.start.submit")}
|
||
</Button>
|
||
</form>
|
||
);
|
||
}
|