mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-27 08:02:27 +00:00
feat: add lead sync frontend clients
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import type {
|
||||
CreateLeadSyncSource,
|
||||
LeadSyncSource,
|
||||
} from "@/lib/api/models/app/leadsync/LeadSync";
|
||||
import Request from "../../Request";
|
||||
|
||||
// Saves a new on-demand source. The backend validates the connection is the
|
||||
// org's google_sheets connection, dedup is valid, and column_mapping maps a
|
||||
// column to 'email'.
|
||||
export default async function createSource(
|
||||
input: CreateLeadSyncSource,
|
||||
): Promise<LeadSyncSource> {
|
||||
return await Request<LeadSyncSource>({
|
||||
method: "POST",
|
||||
url: "/lead-sync/sources",
|
||||
data: input,
|
||||
authorization: true,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import Request from "../../Request";
|
||||
|
||||
// Deletes a saved source (org-scoped). Backend returns 204 No Content.
|
||||
export default async function deleteSource(id: string): Promise<void> {
|
||||
await Request<void>({
|
||||
method: "DELETE",
|
||||
url: `/lead-sync/sources/${id}`,
|
||||
authorization: true,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { LeadSyncConnection } from "@/lib/api/models/app/leadsync/LeadSync";
|
||||
import Request from "../../Request";
|
||||
|
||||
// Reports whether the org has a connected (hidden) google_sheets OAuth
|
||||
// connection usable for lead-sync. Drives "Connect Google" vs the sheet picker.
|
||||
export default async function getGoogleConnection(): Promise<LeadSyncConnection> {
|
||||
return await Request<LeadSyncConnection>({
|
||||
method: "GET",
|
||||
url: "/lead-sync/google/connection",
|
||||
authorization: true,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { LeadSyncSource } from "@/lib/api/models/app/leadsync/LeadSync";
|
||||
import Request from "../../Request";
|
||||
|
||||
// Returns one saved source (org-scoped).
|
||||
export default async function getSource(id: string): Promise<LeadSyncSource> {
|
||||
return await Request<LeadSyncSource>({
|
||||
method: "GET",
|
||||
url: `/lead-sync/sources/${id}`,
|
||||
authorization: true,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { SheetMeta } from "@/lib/api/models/app/leadsync/LeadSync";
|
||||
import Request from "../../Request";
|
||||
|
||||
// Returns a spreadsheet's title + tabs (read via the connection's refreshed
|
||||
// Google token) so the UI can render a tab picker.
|
||||
export default async function getSpreadsheet(input: {
|
||||
connection_id: string;
|
||||
sheet_id: string;
|
||||
}): Promise<SheetMeta> {
|
||||
return await Request<SheetMeta>({
|
||||
method: "POST",
|
||||
url: "/lead-sync/google/spreadsheet",
|
||||
data: input,
|
||||
authorization: true,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { LeadSyncSource } from "@/lib/api/models/app/leadsync/LeadSync";
|
||||
import Request from "../../Request";
|
||||
|
||||
// Lists this org's saved sync sources, optionally filtered to one campaign
|
||||
// (powers both the global Contacts > Sync sources area and the per-campaign
|
||||
// "Connect a Google Sheet" list).
|
||||
export default async function listSources(campaignId?: string): Promise<{ data: LeadSyncSource[] }> {
|
||||
return await Request<{ data: LeadSyncSource[] }>({
|
||||
method: "GET",
|
||||
url: "/lead-sync/sources",
|
||||
params: campaignId ? { campaign_id: campaignId } : undefined,
|
||||
authorization: true,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { ImportPreview } from "@/lib/api/client/app/contacts/importContacts";
|
||||
import Request from "../../Request";
|
||||
|
||||
// Reads the first ~21 rows of a tab and returns the exact contacts
|
||||
// ImportPreview JSON shape, so the frontend reuses its contact-import column
|
||||
// mapper verbatim.
|
||||
export default async function previewSheet(input: {
|
||||
connection_id: string;
|
||||
sheet_id: string;
|
||||
tab_title: string;
|
||||
}): Promise<ImportPreview> {
|
||||
return await Request<ImportPreview>({
|
||||
method: "POST",
|
||||
url: "/lead-sync/google/preview",
|
||||
data: input,
|
||||
authorization: true,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { LeadSyncResult } from "@/lib/api/models/app/leadsync/LeadSync";
|
||||
import Request from "../../Request";
|
||||
|
||||
// "Sync now": reads the full sheet tab, encodes rows as CSV in memory, and runs
|
||||
// the existing contact ImportCommit (upsert by email, attach categories +
|
||||
// target campaign). Naturally idempotent (email upsert), so no Idempotency-Key.
|
||||
export default async function syncSource(id: string): Promise<LeadSyncResult> {
|
||||
return await Request<LeadSyncResult>({
|
||||
method: "POST",
|
||||
url: `/lead-sync/sources/${id}/sync`,
|
||||
authorization: true,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type {
|
||||
LeadSyncSource,
|
||||
UpdateLeadSyncSource,
|
||||
} from "@/lib/api/models/app/leadsync/LeadSync";
|
||||
import Request from "../../Request";
|
||||
|
||||
// Edits a saved source; omitted fields are left unchanged. clear_campaign:true
|
||||
// detaches the target campaign.
|
||||
export default async function updateSource(
|
||||
id: string,
|
||||
input: UpdateLeadSyncSource,
|
||||
): Promise<LeadSyncSource> {
|
||||
return await Request<LeadSyncSource>({
|
||||
method: "PATCH",
|
||||
url: `/lead-sync/sources/${id}`,
|
||||
data: input,
|
||||
authorization: true,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import createSource from "@/lib/api/client/app/leadsync/createSource";
|
||||
|
||||
// Saves a new sync source and refreshes every source list.
|
||||
export default function useCreateLeadSyncSource() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: createSource,
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["lead-sync", "sources"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import deleteSource from "@/lib/api/client/app/leadsync/deleteSource";
|
||||
|
||||
// Deletes a saved source and refreshes every source list.
|
||||
export default function useDeleteLeadSyncSource() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: deleteSource,
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["lead-sync", "sources"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import getGoogleConnection from "@/lib/api/client/app/leadsync/getGoogleConnection";
|
||||
|
||||
// Reports whether the org has a connected google_sheets OAuth connection
|
||||
// usable for lead-sync. Drives the "Connect Google Sheets" vs sheet-picker UI.
|
||||
export default function useGoogleConnection() {
|
||||
return useQuery({
|
||||
queryKey: ["lead-sync", "google", "connection"],
|
||||
queryFn: getGoogleConnection,
|
||||
staleTime: 10_000,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import listSources from "@/lib/api/client/app/leadsync/listSources";
|
||||
|
||||
// Lists saved sync sources, optionally filtered to one campaign. Used by both
|
||||
// the global Contacts > Sync sources area and the per-campaign list.
|
||||
export default function useLeadSyncSources(campaignId?: string) {
|
||||
return useQuery({
|
||||
queryKey: ["lead-sync", "sources", campaignId ?? null],
|
||||
queryFn: () => listSources(campaignId),
|
||||
staleTime: 10_000,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import getSpreadsheet from "@/lib/api/client/app/leadsync/getSpreadsheet";
|
||||
import previewSheet from "@/lib/api/client/app/leadsync/previewSheet";
|
||||
|
||||
// Fetching spreadsheet metadata and previewing a tab are user-triggered
|
||||
// actions inside the wizard (not background queries), so they're mutations.
|
||||
export function useGetSpreadsheet() {
|
||||
return useMutation({ mutationFn: getSpreadsheet });
|
||||
}
|
||||
|
||||
export function usePreviewSheet() {
|
||||
return useMutation({ mutationFn: previewSheet });
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import syncSource from "@/lib/api/client/app/leadsync/syncSource";
|
||||
|
||||
// "Sync now". On success, refreshes the source list (so last_synced_at /
|
||||
// last_result / status update) and contacts (new/updated rows landed).
|
||||
export default function useSyncLeadSyncSource() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: syncSource,
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["lead-sync", "sources"] });
|
||||
qc.invalidateQueries({ queryKey: ["contacts"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import updateSource from "@/lib/api/client/app/leadsync/updateSource";
|
||||
import type { UpdateLeadSyncSource } from "@/lib/api/models/app/leadsync/LeadSync";
|
||||
|
||||
// Edits a saved source and refreshes every source list.
|
||||
export default function useUpdateLeadSyncSource() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (input: { id: string; body: UpdateLeadSyncSource }) =>
|
||||
updateSource(input.id, input.body),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["lead-sync", "sources"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// Mirror of the backend's models/lead_sync.go shapes. Field names match the
|
||||
// JSON contract exactly. Lead-sync is an on-demand Google Sheet → contacts
|
||||
// importer: a saved "sync source" the user re-runs via a "Sync now" button.
|
||||
//
|
||||
// We deliberately REUSE the contact-import column-mapping + result types from
|
||||
// the import wizard rather than redeclaring them, so the same TargetPicker /
|
||||
// MapStep mapper and result rendering work verbatim against /google/preview
|
||||
// and /sources/:id/sync responses.
|
||||
|
||||
import type {
|
||||
ImportColumnMapping,
|
||||
ImportDedupStrategy,
|
||||
ImportPreview,
|
||||
ImportResult,
|
||||
} from "@/lib/api/client/app/contacts/importContacts";
|
||||
|
||||
// Re-export the reused contact-import types under lead-sync names so callers
|
||||
// can import everything lead-sync from one module.
|
||||
export type {
|
||||
ImportColumnMapping,
|
||||
ImportDedupStrategy,
|
||||
ImportPreview,
|
||||
ImportResult,
|
||||
};
|
||||
|
||||
// LeadSyncStatus mirrors models.LeadSyncStatus. The synchronous SyncNow only
|
||||
// ever lands on idle/error; "syncing" exists for completeness/future async use.
|
||||
export type LeadSyncStatus = "idle" | "syncing" | "error";
|
||||
|
||||
// A saved on-demand Google-Sheet → contacts source.
|
||||
export interface LeadSyncSource {
|
||||
id: string;
|
||||
organization_id: string;
|
||||
created_by_user_id: string;
|
||||
provider: string;
|
||||
connection_id: string;
|
||||
sheet_id: string;
|
||||
sheet_title?: string;
|
||||
tab_title?: string;
|
||||
a1_range?: string;
|
||||
has_header: boolean;
|
||||
column_mapping: ImportColumnMapping[];
|
||||
dedup: ImportDedupStrategy;
|
||||
target_campaign_id?: string;
|
||||
category_ids: string[];
|
||||
subscribed_default: boolean;
|
||||
label?: string;
|
||||
status: LeadSyncStatus;
|
||||
last_synced_at?: string;
|
||||
last_result?: ImportResult;
|
||||
last_error?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// Result of POST /sources/:id/sync.
|
||||
export interface LeadSyncResult {
|
||||
source_id: string;
|
||||
result: ImportResult;
|
||||
}
|
||||
|
||||
// Reports whether the org has a connected (hidden) google_sheets OAuth
|
||||
// connection usable for lead-sync.
|
||||
export interface LeadSyncConnection {
|
||||
connected: boolean;
|
||||
connection: {
|
||||
id: string;
|
||||
external_account_name: string;
|
||||
status: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
// One tab of a spreadsheet, from /google/spreadsheet.
|
||||
export interface SheetTab {
|
||||
title: string;
|
||||
index: number;
|
||||
}
|
||||
|
||||
// Spreadsheet metadata returned by /google/spreadsheet — drives the tab picker.
|
||||
export interface SheetMeta {
|
||||
sheet_id: string;
|
||||
title: string;
|
||||
tabs: SheetTab[];
|
||||
}
|
||||
|
||||
// Request body for creating a new source.
|
||||
export interface CreateLeadSyncSource {
|
||||
connection_id: string;
|
||||
sheet_id: string;
|
||||
sheet_title: string;
|
||||
tab_title: string;
|
||||
has_header: boolean;
|
||||
column_mapping: ImportColumnMapping[];
|
||||
dedup: ImportDedupStrategy;
|
||||
target_campaign_id?: string;
|
||||
category_ids: string[];
|
||||
subscribed_default?: boolean;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// Request body for PATCH — all fields optional; omitted ⇒ unchanged.
|
||||
// `clear_campaign: true` detaches the target campaign (a nil pointer alone
|
||||
// can't express "clear").
|
||||
export interface UpdateLeadSyncSource {
|
||||
sheet_id?: string;
|
||||
sheet_title?: string;
|
||||
tab_title?: string;
|
||||
has_header?: boolean;
|
||||
column_mapping?: ImportColumnMapping[];
|
||||
dedup?: ImportDedupStrategy;
|
||||
target_campaign_id?: string;
|
||||
clear_campaign?: boolean;
|
||||
category_ids?: string[];
|
||||
subscribed_default?: boolean;
|
||||
label?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user