2026-08-19 07:37:44 +00:00
|
|
|
import { useState } from "react";
|
|
|
|
|
import type { IntentBand, Opportunity } from "../../domain/types";
|
2026-08-18 16:26:10 +00:00
|
|
|
import { useI18n } from "../../i18n/I18nContext";
|
2026-08-13 02:22:24 +00:00
|
|
|
import { Badge, Button } from "../ui";
|
2026-08-19 07:37:44 +00:00
|
|
|
import { PrimaryProductPicker } from "./PrimaryProductPicker";
|
2026-08-13 02:22:24 +00:00
|
|
|
import { ProductMatchDetails } from "./ProductMatchDetails";
|
2026-08-19 07:37:44 +00:00
|
|
|
import { ReplyComposer } from "./ReplyComposer";
|
|
|
|
|
|
|
|
|
|
const BANDS: IntentBand[] = ["high", "mid", "low"];
|
2026-08-13 02:22:24 +00:00
|
|
|
|
|
|
|
|
type Props = {
|
|
|
|
|
opportunity: Opportunity;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
onAccept?: (opportunity: Opportunity) => void;
|
|
|
|
|
onComplete?: (opportunity: Opportunity) => void;
|
2026-08-19 07:37:44 +00:00
|
|
|
/** 改主推產品(多產品匹配時);沒給就不顯示。 */
|
|
|
|
|
onSetPrimary?: (opportunity: Opportunity, productId: string, reason: string) => void;
|
|
|
|
|
/** 覆寫意向分級;沒給就不顯示。 */
|
|
|
|
|
onOverrideBand?: (opportunity: Opportunity, band: IntentBand) => void;
|
2026-08-13 02:22:24 +00:00
|
|
|
busy?: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
2026-08-19 07:37:44 +00:00
|
|
|
export function OpportunityDetailDrawer({
|
|
|
|
|
opportunity,
|
|
|
|
|
onClose,
|
|
|
|
|
onAccept,
|
|
|
|
|
onComplete,
|
|
|
|
|
onSetPrimary,
|
|
|
|
|
onOverrideBand,
|
|
|
|
|
busy = false,
|
|
|
|
|
}: Props) {
|
2026-08-18 16:26:10 +00:00
|
|
|
const { t } = useI18n();
|
2026-08-19 07:37:44 +00:00
|
|
|
const [overrideOpen, setOverrideOpen] = useState(false);
|
2026-08-13 02:22:24 +00:00
|
|
|
const pending = (opportunity.review_state || "pending") === "pending";
|
|
|
|
|
const accepted = opportunity.status === "accepted" || Boolean(opportunity.contact_id);
|
2026-08-19 07:37:44 +00:00
|
|
|
const matches = opportunity.product_matches ?? [];
|
2026-08-13 02:22:24 +00:00
|
|
|
return (
|
2026-08-18 16:26:10 +00:00
|
|
|
<aside className="hb-opp-drawer" role="dialog" aria-modal="true" aria-label={t("radar.drawer.aria")}>
|
2026-08-13 02:22:24 +00:00
|
|
|
<div className="hb-opp-drawer__head">
|
|
|
|
|
<div>
|
2026-08-18 16:26:10 +00:00
|
|
|
<span className="hb-radar-section__hint">{t("radar.drawer.title")}</span>
|
|
|
|
|
<h2>{opportunity.primary_product_label || t("radar.drawer.noProduct")}</h2>
|
2026-08-13 02:22:24 +00:00
|
|
|
</div>
|
2026-08-18 16:26:10 +00:00
|
|
|
<Button type="button" variant="ghost" aria-label={t("radar.drawer.closeAria")} onClick={onClose}>{t("radar.drawer.close")}</Button>
|
2026-08-13 02:22:24 +00:00
|
|
|
</div>
|
|
|
|
|
<div className="hb-opp-drawer__body">
|
|
|
|
|
<p className="hb-opp-card__text">{opportunity.text}</p>
|
|
|
|
|
<div className="hb-opp-card__meta">
|
2026-08-18 16:26:10 +00:00
|
|
|
<Badge tone="brand">{t("radar.drawer.intent", { n: opportunity.intent_score })}</Badge>
|
|
|
|
|
{opportunity.priority_score !== undefined ? <Badge tone="warning">{t("radar.drawer.priority", { n: opportunity.priority_score })}</Badge> : null}
|
|
|
|
|
<span>@{opportunity.author_handle || t("radar.card.unknownAuthor")}</span>
|
2026-08-13 02:22:24 +00:00
|
|
|
</div>
|
2026-08-19 07:37:44 +00:00
|
|
|
|
|
|
|
|
{/* 回覆放在證據上方:看完原文最想做的事就是回他。 */}
|
|
|
|
|
<section className="hb-opp-drawer__section">
|
|
|
|
|
<h3>{t("radar.drawer.reply")}</h3>
|
|
|
|
|
<ReplyComposer opportunity={opportunity} />
|
|
|
|
|
</section>
|
|
|
|
|
|
2026-08-13 02:22:24 +00:00
|
|
|
{opportunity.demand_evidence?.length ? (
|
2026-08-18 16:26:10 +00:00
|
|
|
<section className="hb-opp-drawer__section"><h3>{t("radar.drawer.evidence")}</h3><ul>{opportunity.demand_evidence.map((item) => <li key={item}>{item}</li>)}</ul></section>
|
2026-08-13 02:22:24 +00:00
|
|
|
) : null}
|
|
|
|
|
<section className="hb-opp-drawer__section">
|
2026-08-18 16:26:10 +00:00
|
|
|
<h3>{t("radar.drawer.matches")}</h3>
|
2026-08-19 07:37:44 +00:00
|
|
|
{matches.length ? matches.map((match) => <ProductMatchDetails key={match.product_id} match={match} />) : <p className="hb-radar-section__hint">{t("radar.drawer.generic")}</p>}
|
|
|
|
|
{matches.length > 1 && onSetPrimary ? (
|
|
|
|
|
<PrimaryProductPicker
|
|
|
|
|
matches={matches}
|
|
|
|
|
currentId={opportunity.primary_product_id}
|
|
|
|
|
busy={busy}
|
|
|
|
|
onSet={(productId, reason) => onSetPrimary(opportunity, productId, reason)}
|
|
|
|
|
/>
|
|
|
|
|
) : null}
|
2026-08-13 02:22:24 +00:00
|
|
|
</section>
|
2026-08-18 16:26:10 +00:00
|
|
|
{opportunity.reasons.length ? <section className="hb-opp-drawer__section"><h3>{t("radar.drawer.judge")}</h3><div className="hb-opp-reasons">{opportunity.reasons.map((reason) => <div className="hb-opp-reason" key={reason.dimension}><strong>{reason.dimension}</strong><span>{reason.score}</span><span>{reason.reason}</span></div>)}</div></section> : null}
|
2026-08-13 02:22:24 +00:00
|
|
|
<div className="hb-opp-drawer__actions">
|
|
|
|
|
{pending && onComplete ? (
|
2026-08-18 16:26:10 +00:00
|
|
|
<Button type="button" disabled={busy} onClick={() => onComplete(opportunity)}>{t("radar.card.keep")}</Button>
|
2026-08-13 07:54:25 +00:00
|
|
|
) : null}
|
|
|
|
|
{pending && !accepted && onAccept ? (
|
2026-08-18 16:26:10 +00:00
|
|
|
<Button type="button" variant="ghost" disabled={busy} onClick={() => onAccept(opportunity)}>{t("radar.card.accept")}</Button>
|
2026-08-13 02:22:24 +00:00
|
|
|
) : null}
|
2026-08-18 16:26:10 +00:00
|
|
|
<a className="hb-btn hb-btn--ghost" href={opportunity.permalink} target="_blank" rel="noreferrer">{t("radar.drawer.openOriginal")}</a>
|
2026-08-19 07:37:44 +00:00
|
|
|
{onOverrideBand ? (
|
|
|
|
|
<Button type="button" variant="ghost" aria-expanded={overrideOpen} onClick={() => setOverrideOpen((value) => !value)}>
|
|
|
|
|
{t("radar.today.action.override")}
|
|
|
|
|
</Button>
|
|
|
|
|
) : null}
|
2026-08-13 02:22:24 +00:00
|
|
|
</div>
|
2026-08-19 07:37:44 +00:00
|
|
|
{overrideOpen && onOverrideBand ? (
|
|
|
|
|
<div className="hb-radar-actions">
|
|
|
|
|
{BANDS.map((band) => (
|
|
|
|
|
<Button
|
|
|
|
|
key={band}
|
|
|
|
|
type="button"
|
|
|
|
|
variant="secondary"
|
|
|
|
|
disabled={busy || opportunity.intent_band === band}
|
|
|
|
|
onClick={() => onOverrideBand(opportunity, band)}
|
|
|
|
|
>
|
|
|
|
|
{t(`radar.today.band.${band}`)}
|
|
|
|
|
</Button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
2026-08-18 16:26:10 +00:00
|
|
|
{pending ? <p className="hb-field__hint">{t("radar.drawer.hint")}</p> : null}
|
2026-08-13 02:22:24 +00:00
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
);
|
|
|
|
|
}
|