mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
feat(automations): add deal pipeline selectors
This commit is contained in:
parent
b2ed58a41f
commit
377c1b738a
@ -167,7 +167,7 @@ function blankConfig(type: AutomationStepType): Record<string, unknown> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
// Account resources (tags, members, approved templates)
|
// Account resources (tags, members, approved templates, pipelines)
|
||||||
//
|
//
|
||||||
// Loaded once at the builder root and shared via context so the
|
// Loaded once at the builder root and shared via context so the
|
||||||
// tag / agent / template pickers below can offer existing resources
|
// tag / agent / template pickers below can offer existing resources
|
||||||
@ -181,6 +181,20 @@ interface AutomationResources {
|
|||||||
members: AccountMember[]
|
members: AccountMember[]
|
||||||
templates: MessageTemplate[]
|
templates: MessageTemplate[]
|
||||||
customFields: CustomField[]
|
customFields: CustomField[]
|
||||||
|
pipelines: PipelineOption[]
|
||||||
|
stages: PipelineStageOption[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PipelineOption {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PipelineStageOption {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
pipeline_id: string
|
||||||
|
position: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const ResourcesContext = createContext<AutomationResources>({
|
const ResourcesContext = createContext<AutomationResources>({
|
||||||
@ -188,6 +202,8 @@ const ResourcesContext = createContext<AutomationResources>({
|
|||||||
members: [],
|
members: [],
|
||||||
templates: [],
|
templates: [],
|
||||||
customFields: [],
|
customFields: [],
|
||||||
|
pipelines: [],
|
||||||
|
stages: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
function useResources(): AutomationResources {
|
function useResources(): AutomationResources {
|
||||||
@ -199,6 +215,8 @@ function ResourcesProvider({ children }: { children: ReactNode }) {
|
|||||||
const [members, setMembers] = useState<AccountMember[]>([])
|
const [members, setMembers] = useState<AccountMember[]>([])
|
||||||
const [templates, setTemplates] = useState<MessageTemplate[]>([])
|
const [templates, setTemplates] = useState<MessageTemplate[]>([])
|
||||||
const [customFields, setCustomFields] = useState<CustomField[]>([])
|
const [customFields, setCustomFields] = useState<CustomField[]>([])
|
||||||
|
const [pipelines, setPipelines] = useState<PipelineOption[]>([])
|
||||||
|
const [stages, setStages] = useState<PipelineStageOption[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
@ -209,19 +227,27 @@ function ResourcesProvider({ children }: { children: ReactNode }) {
|
|||||||
// actually be sent (anything else 400s at send time), matching the
|
// actually be sent (anything else 400s at send time), matching the
|
||||||
// broadcast picker.
|
// broadcast picker.
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const [tagsRes, templatesRes, customFieldsRes] = await Promise.all([
|
const [tagsRes, templatesRes, customFieldsRes, pipelinesRes, stagesRes] =
|
||||||
supabase.from("tags").select("*").order("name"),
|
await Promise.all([
|
||||||
supabase
|
supabase.from("tags").select("*").order("name"),
|
||||||
.from("message_templates")
|
supabase
|
||||||
.select("*")
|
.from("message_templates")
|
||||||
.eq("status", "APPROVED")
|
.select("*")
|
||||||
.order("name"),
|
.eq("status", "APPROVED")
|
||||||
supabase.from("custom_fields").select("*").order("field_name"),
|
.order("name"),
|
||||||
])
|
supabase.from("custom_fields").select("*").order("field_name"),
|
||||||
|
supabase.from("pipelines").select("id, name").order("name"),
|
||||||
|
supabase
|
||||||
|
.from("pipeline_stages")
|
||||||
|
.select("id, name, pipeline_id, position")
|
||||||
|
.order("position"),
|
||||||
|
])
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
setTags((tagsRes.data as TagRecord[] | null) ?? [])
|
setTags((tagsRes.data as TagRecord[] | null) ?? [])
|
||||||
setTemplates((templatesRes.data as MessageTemplate[] | null) ?? [])
|
setTemplates((templatesRes.data as MessageTemplate[] | null) ?? [])
|
||||||
setCustomFields((customFieldsRes.data as CustomField[] | null) ?? [])
|
setCustomFields((customFieldsRes.data as CustomField[] | null) ?? [])
|
||||||
|
setPipelines((pipelinesRes.data as PipelineOption[] | null) ?? [])
|
||||||
|
setStages((stagesRes.data as PipelineStageOption[] | null) ?? [])
|
||||||
})()
|
})()
|
||||||
|
|
||||||
// Members go through the API so we inherit its email-visibility
|
// Members go through the API so we inherit its email-visibility
|
||||||
@ -244,7 +270,9 @@ function ResourcesProvider({ children }: { children: ReactNode }) {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResourcesContext.Provider value={{ tags, members, templates, customFields }}>
|
<ResourcesContext.Provider
|
||||||
|
value={{ tags, members, templates, customFields, pipelines, stages }}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</ResourcesContext.Provider>
|
</ResourcesContext.Provider>
|
||||||
)
|
)
|
||||||
@ -382,6 +410,102 @@ function AgentSelect({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Pipeline + stage picker for Create Deal. The automation stores ids because
|
||||||
|
* the engine writes directly to deals, but authors should choose by name. */
|
||||||
|
function DealPipelineFields({
|
||||||
|
pipelineId,
|
||||||
|
stageId,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
pipelineId: string
|
||||||
|
stageId: string
|
||||||
|
onChange: (patch: { pipeline_id: string; stage_id: string }) => void
|
||||||
|
}) {
|
||||||
|
const { pipelines, stages } = useResources()
|
||||||
|
|
||||||
|
if (pipelines.length === 0) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FieldBlock label="Pipeline id">
|
||||||
|
<Input
|
||||||
|
value={pipelineId}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange({ pipeline_id: e.target.value, stage_id: stageId })
|
||||||
|
}
|
||||||
|
className="bg-muted text-foreground"
|
||||||
|
/>
|
||||||
|
</FieldBlock>
|
||||||
|
<FieldBlock label="Stage id">
|
||||||
|
<Input
|
||||||
|
value={stageId}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange({ pipeline_id: pipelineId, stage_id: e.target.value })
|
||||||
|
}
|
||||||
|
className="bg-muted text-foreground"
|
||||||
|
/>
|
||||||
|
</FieldBlock>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedPipeline = pipelines.find((p) => p.id === pipelineId)
|
||||||
|
const stageOptions = stages.filter((s) => s.pipeline_id === pipelineId)
|
||||||
|
const selectedStage = stageOptions.find((s) => s.id === stageId)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FieldBlock label="Pipeline">
|
||||||
|
<select
|
||||||
|
value={pipelineId}
|
||||||
|
onChange={(e) => {
|
||||||
|
const nextPipelineId = e.target.value
|
||||||
|
const firstStage = stages.find(
|
||||||
|
(s) => s.pipeline_id === nextPipelineId
|
||||||
|
)
|
||||||
|
onChange({
|
||||||
|
pipeline_id: nextPipelineId,
|
||||||
|
stage_id: firstStage?.id ?? "",
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
className={SELECT_CLASS}
|
||||||
|
>
|
||||||
|
<option value="">Select a pipeline…</option>
|
||||||
|
{pipelines.map((p) => (
|
||||||
|
<option key={p.id} value={p.id}>
|
||||||
|
{p.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
{pipelineId && !selectedPipeline && (
|
||||||
|
<option value={pipelineId}>{pipelineId} (unknown pipeline)</option>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</FieldBlock>
|
||||||
|
<FieldBlock label="Stage">
|
||||||
|
<select
|
||||||
|
value={stageId}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange({ pipeline_id: pipelineId, stage_id: e.target.value })
|
||||||
|
}
|
||||||
|
className={SELECT_CLASS}
|
||||||
|
disabled={!pipelineId || stageOptions.length === 0}
|
||||||
|
>
|
||||||
|
<option value="">
|
||||||
|
{pipelineId ? "Select a stage…" : "Select a pipeline first…"}
|
||||||
|
</option>
|
||||||
|
{stageOptions.map((s) => (
|
||||||
|
<option key={s.id} value={s.id}>
|
||||||
|
{s.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
{stageId && pipelineId && !selectedStage && (
|
||||||
|
<option value={stageId}>{stageId} (unknown stage)</option>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</FieldBlock>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** Template dropdown showing approved templates by name + language,
|
/** Template dropdown showing approved templates by name + language,
|
||||||
* storing both template_name and language. Falls back to manual name +
|
* storing both template_name and language. Falls back to manual name +
|
||||||
* language inputs when no approved templates are synced yet. */
|
* language inputs when no approved templates are synced yet. */
|
||||||
@ -1125,20 +1249,11 @@ function StepEditor({
|
|||||||
case "create_deal":
|
case "create_deal":
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FieldBlock label="Pipeline id">
|
<DealPipelineFields
|
||||||
<Input
|
pipelineId={(cfg.pipeline_id as string) ?? ""}
|
||||||
value={(cfg.pipeline_id as string) ?? ""}
|
stageId={(cfg.stage_id as string) ?? ""}
|
||||||
onChange={(e) => set({ pipeline_id: e.target.value })}
|
onChange={(patch) => set(patch)}
|
||||||
className="bg-muted text-foreground"
|
/>
|
||||||
/>
|
|
||||||
</FieldBlock>
|
|
||||||
<FieldBlock label="Stage id">
|
|
||||||
<Input
|
|
||||||
value={(cfg.stage_id as string) ?? ""}
|
|
||||||
onChange={(e) => set({ stage_id: e.target.value })}
|
|
||||||
className="bg-muted text-foreground"
|
|
||||||
/>
|
|
||||||
</FieldBlock>
|
|
||||||
<FieldBlock label="Title">
|
<FieldBlock label="Title">
|
||||||
<Input
|
<Input
|
||||||
value={(cfg.title as string) ?? ""}
|
value={(cfg.title as string) ?? ""}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user