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