import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, } from "@/components/ui/sheet"; import { Textarea } from "@/components/ui/textarea"; import { ChipQuestion } from "./components/chip-question"; import { LabeledField } from "./components/field-label"; import { ScreenshotUpload } from "./components/screenshot-upload"; import { TYPE_META } from "./display-meta"; import type { FeedbackType } from "./types"; /** * Request type — derived from `TYPE_META` so the chips stay in sync with the * card's TypeBadge (Idea / Enhancement / Bug). Single-select. */ const TYPE_OPTIONS = (Object.keys(TYPE_META) as FeedbackType[]).map((t) => ({ value: t, label: TYPE_META[t].label, })); /** Sentinel for the "Other" area — only offered for ideas (see below). */ const OTHER_AREA = "__other__"; /** * Backoffice features a request can relate to — mirrors the Backoffice sidebar * navigation. Shown as a dropdown. */ const AREA_OPTIONS = [ { value: "Contacts", label: "Clients" }, { value: "Companies", label: "Companies" }, { value: "Loan CRM", label: "Loan CRM" }, { value: "Conversations", label: "Conversations" }, { value: "Appointments", label: "Appointments" }, { value: "WealthX AI", label: "WealthX AI" }, { value: "Resource Centre", label: "Resource Centre" }, { value: "Staff", label: "Staff" }, { value: "Web Pages", label: "Web Pages" }, { value: "Account", label: "Account" }, ]; /* ---- Bug questions: the signals a triager needs to reproduce + rank it ---- */ const BUG_FREQUENCY_OPTIONS = [ { value: "every-time", label: "Every time" }, { value: "sometimes", label: "Sometimes" }, { value: "once", label: "Only once" }, ]; const BUG_IMPACT_OPTIONS = [ { value: "blocked", label: "I'm blocked" }, { value: "workaround", label: "I have a workaround" }, { value: "minor", label: "Minor annoyance" }, ]; /* ---- Idea / enhancement questions: the demand + value signals ---- */ const BENEFICIARY_OPTIONS = [ { value: "me", label: "Me" }, { value: "team", label: "My team" }, { value: "clients", label: "My clients" }, ]; export interface FeedbackNewRequestPayload { title: string; type: FeedbackType; area: string; problemStatement: string; stepsToReproduce?: string; frequency?: string; impact?: string; proposedSolution?: string; workaround?: string; beneficiaries?: string[]; previewImage?: File | null; } export interface FeedbackNewRequestProps { open: boolean; onOpenChange: (open: boolean) => void; /** * Fired when the user submits the intake form. May return a promise — the form * awaits it and keeps the submit button busy until it settles. On success the * host closes the drawer; if it rejects, the form stays open and `onError` * fires. The form never closes on its own. */ onSubmit?: (payload: FeedbackNewRequestPayload) => void | Promise; /** * Fired when `onSubmit` rejects. The drawer stays open so the user can retry — * the host decides how (or whether) to surface the failure. */ onError?: (error: unknown) => void; } /** * Broker-facing intake form, presented as a right-side slide-out. The follow-up * questions adapt to the request type: a **bug** asks for reproduction + * frequency + impact, while an **idea / enhancement** asks for the desired * outcome, the current workaround, and who it benefits. */ export function FeedbackNewRequest({ open, onOpenChange, onSubmit, onError, }: FeedbackNewRequestProps) { const [title, setTitle] = useState(""); const [type, setType] = useState(""); const [area, setArea] = useState(""); const [otherArea, setOtherArea] = useState(""); const [problem, setProblem] = useState(""); // Bug-specific const [steps, setSteps] = useState(""); const [bugFrequency, setBugFrequency] = useState(""); const [bugImpact, setBugImpact] = useState(""); // Idea / enhancement-specific const [outcome, setOutcome] = useState(""); const [workaround, setWorkaround] = useState(""); const [beneficiaries, setBeneficiaries] = useState([]); const [screenshot, setScreenshot] = useState(null); // In-flight while the host resolves the submission — blocks double-submits. const [submitting, setSubmitting] = useState(false); const isBug = type === "bug"; const isImprovement = type === "idea" || type === "enhancement"; // "Other" area is an escape hatch for ideas that don't map to an existing // Backoffice feature — only offered when the request is an idea. const areaOptions = type === "idea" ? [...AREA_OPTIONS, { value: OTHER_AREA, label: "Other (not listed)" }] : AREA_OPTIONS; function handleTypeChange(next: string) { setType(next); // Drop the "Other" selection if it's no longer offered for this type. if (next !== "idea" && area === OTHER_AREA) { setArea(""); setOtherArea(""); } } const resolvedArea = area === OTHER_AREA ? otherArea.trim() : area; const areaComplete = area !== "" && (area !== OTHER_AREA || otherArea.trim() !== ""); const canSubmit = title.trim() !== "" && Boolean(type) && areaComplete && problem.trim() !== ""; const buildPayload = (): FeedbackNewRequestPayload => { const payload: FeedbackNewRequestPayload = { title: title.trim(), type: type as FeedbackType, area: resolvedArea, problemStatement: problem.trim(), }; if (isBug) { if (steps.trim()) payload.stepsToReproduce = steps.trim(); if (bugFrequency) { const match = BUG_FREQUENCY_OPTIONS.find((o) => o.value === bugFrequency); payload.frequency = match?.label ?? bugFrequency; } if (bugImpact) { const match = BUG_IMPACT_OPTIONS.find((o) => o.value === bugImpact); payload.impact = match?.label ?? bugImpact; } } if (isImprovement) { if (outcome.trim()) payload.proposedSolution = outcome.trim(); if (workaround.trim()) payload.workaround = workaround.trim(); if (beneficiaries.length > 0) { payload.beneficiaries = beneficiaries.map((v) => { const match = BENEFICIARY_OPTIONS.find((o) => o.value === v); return match?.label ?? v; }); } } if (screenshot) payload.previewImage = screenshot; return payload; }; // Forward the payload and await the host. On success the host closes the // drawer; on failure we emit `onError` and stay open (we never close // ourselves), so the user can retry. Catching also keeps the rejection from // going unhandled. async function handleSubmit() { if (submitting) return; setSubmitting(true); try { await onSubmit?.(buildPayload()); } catch (error) { onError?.(error); } finally { setSubmitting(false); } } return ( New request Tell us what's going on. The more detail you give, the faster we can act on it.
setTitle(e.target.value)} /> handleTypeChange(v as string)} /> {/* Ideas may not map to an existing feature — let them describe it. */} {area === OTHER_AREA && ( setOtherArea(e.target.value)} /> )} {/* ---- Bug: reproduce + rank ---- */} {isBug && ( <>