Connect all CRM pages to the BFF and CRM service, add module docs, and mark CRM available in the SuperApp catalog. Co-authored-by: Cursor <cursoragent@cursor.com>
735 lines
26 KiB
TypeScript
735 lines
26 KiB
TypeScript
/**
|
||
* CRM Service API client — real backend only (Phase 6.0–6.3).
|
||
* Browser → /api/crm BFF; SSR → direct upstream.
|
||
*/
|
||
|
||
import { getValidAccessToken, refreshSession } from "@/lib/auth";
|
||
import type {
|
||
Attachment,
|
||
AuditEntry,
|
||
CallLog,
|
||
Comment,
|
||
Contact,
|
||
CustomField,
|
||
Forecast,
|
||
Goal,
|
||
HealthResponse,
|
||
Lead,
|
||
ListParams,
|
||
LookupRead,
|
||
Meeting,
|
||
Mention,
|
||
Note,
|
||
Opportunity,
|
||
Organization,
|
||
Pipeline,
|
||
PipelineStage,
|
||
Playbook,
|
||
Quote,
|
||
SalesActivity,
|
||
Tag,
|
||
Target,
|
||
Task,
|
||
TeamMember,
|
||
TimelineEvent,
|
||
} from "@/modules/crm/types";
|
||
|
||
const CRM_BASE =
|
||
typeof window !== "undefined"
|
||
? "/api/crm"
|
||
: process.env.CRM_SERVICE_URL ||
|
||
process.env.NEXT_PUBLIC_CRM_API_URL ||
|
||
"http://localhost:8003";
|
||
|
||
export class CrmApiError extends Error {
|
||
constructor(
|
||
public status: number,
|
||
public code: string,
|
||
message: string
|
||
) {
|
||
super(message);
|
||
this.name = "CrmApiError";
|
||
}
|
||
}
|
||
|
||
type RequestOptions = RequestInit & { tenantId: string; auth?: boolean };
|
||
|
||
async function request<T>(path: string, options: RequestOptions): Promise<T> {
|
||
const { tenantId, auth = true, headers: customHeaders, ...init } = options;
|
||
const headers = new Headers(customHeaders);
|
||
if (!headers.has("Content-Type") && init.body) {
|
||
headers.set("Content-Type", "application/json");
|
||
}
|
||
headers.set("X-Tenant-ID", tenantId);
|
||
if (auth) {
|
||
const token = await getValidAccessToken();
|
||
if (token) headers.set("Authorization", `Bearer ${token}`);
|
||
}
|
||
|
||
const url = path.startsWith("http")
|
||
? path
|
||
: `${CRM_BASE.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
||
|
||
let res: Response;
|
||
try {
|
||
res = await fetch(url, { ...init, headers });
|
||
} catch (err) {
|
||
const detail = err instanceof Error ? err.message : "network_error";
|
||
throw new Error(`اتصال به سرویس CRM برقرار نشد (${url}) — ${detail}`);
|
||
}
|
||
|
||
if (res.status === 401 && auth) {
|
||
const refreshed = await refreshSession({ force: true });
|
||
if (refreshed) {
|
||
headers.set("Authorization", `Bearer ${refreshed}`);
|
||
res = await fetch(url, { ...init, headers });
|
||
}
|
||
}
|
||
|
||
if (!res.ok) {
|
||
const body = await res.json().catch(() => ({}));
|
||
throw new CrmApiError(
|
||
res.status,
|
||
body?.error?.code || body?.detail || "unknown_error",
|
||
body?.error?.message || body?.detail || res.statusText
|
||
);
|
||
}
|
||
if (res.status === 204) return undefined as T;
|
||
return res.json() as Promise<T>;
|
||
}
|
||
|
||
function qs(params?: ListParams) {
|
||
if (!params) return "";
|
||
const sp = new URLSearchParams();
|
||
Object.entries(params).forEach(([k, v]) => {
|
||
if (v !== undefined && v !== null && v !== "") sp.set(k, String(v));
|
||
});
|
||
const s = sp.toString();
|
||
return s ? `?${s}` : "";
|
||
}
|
||
|
||
function apiPath(segment: string) {
|
||
return `/api/v1${segment.startsWith("/") ? segment : `/${segment}`}`;
|
||
}
|
||
|
||
export const crmApi = {
|
||
health: () =>
|
||
request<HealthResponse>("/health", {
|
||
tenantId: "00000000-0000-0000-0000-000000000000",
|
||
auth: false,
|
||
}),
|
||
|
||
leads: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<Lead[]>(`${apiPath("/leads")}${qs(params)}`, { tenantId }),
|
||
get: (tenantId: string, id: string) =>
|
||
request<Lead>(apiPath(`/leads/${id}`), { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Lead>(apiPath("/leads"), { tenantId, method: "POST", body: JSON.stringify(body) }),
|
||
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<Lead>(apiPath(`/leads/${id}`), {
|
||
tenantId,
|
||
method: "PATCH",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
assign: (tenantId: string, id: string, body: { assigned_user_id: string; note?: string }) =>
|
||
request<Lead>(apiPath(`/leads/${id}/assign`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
delete: (tenantId: string, id: string) =>
|
||
request<Lead>(apiPath(`/leads/${id}/delete`), { tenantId, method: "POST" }),
|
||
restore: (tenantId: string, id: string) =>
|
||
request<Lead>(apiPath(`/leads/${id}/restore`), { tenantId, method: "POST" }),
|
||
},
|
||
|
||
contacts: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<Contact[]>(`${apiPath("/contacts")}${qs(params)}`, { tenantId }),
|
||
get: (tenantId: string, id: string) =>
|
||
request<Contact>(apiPath(`/contacts/${id}`), { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Contact>(apiPath("/contacts"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<Contact>(apiPath(`/contacts/${id}`), {
|
||
tenantId,
|
||
method: "PATCH",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
delete: (tenantId: string, id: string) =>
|
||
request<Contact>(apiPath(`/contacts/${id}/delete`), { tenantId, method: "POST" }),
|
||
},
|
||
|
||
organizations: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<Organization[]>(`${apiPath("/organizations")}${qs(params)}`, { tenantId }),
|
||
get: (tenantId: string, id: string) =>
|
||
request<Organization>(apiPath(`/organizations/${id}`), { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Organization>(apiPath("/organizations"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<Organization>(apiPath(`/organizations/${id}`), {
|
||
tenantId,
|
||
method: "PATCH",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
delete: (tenantId: string, id: string) =>
|
||
request<Organization>(apiPath(`/organizations/${id}/delete`), {
|
||
tenantId,
|
||
method: "POST",
|
||
}),
|
||
},
|
||
|
||
opportunities: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<Opportunity[]>(`${apiPath("/opportunities")}${qs(params)}`, { tenantId }),
|
||
get: (tenantId: string, id: string) =>
|
||
request<Opportunity>(apiPath(`/opportunities/${id}`), { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Opportunity>(apiPath("/opportunities"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<Opportunity>(apiPath(`/opportunities/${id}`), {
|
||
tenantId,
|
||
method: "PATCH",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
changeStage: (tenantId: string, id: string, body: { stage_id: string; note?: string }) =>
|
||
request<Opportunity>(apiPath(`/opportunities/${id}/stage`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
win: (tenantId: string, id: string, body?: Record<string, unknown>) =>
|
||
request<Opportunity>(apiPath(`/opportunities/${id}/win`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body ?? {}),
|
||
}),
|
||
lose: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<Opportunity>(apiPath(`/opportunities/${id}/lose`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
stageHistory: (tenantId: string, id: string) =>
|
||
request<Record<string, unknown>[]>(apiPath(`/opportunities/${id}/stage-history`), {
|
||
tenantId,
|
||
}),
|
||
},
|
||
|
||
pipelines: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<Pipeline[]>(`${apiPath("/pipelines")}${qs(params)}`, { tenantId }),
|
||
get: (tenantId: string, id: string) =>
|
||
request<Pipeline>(apiPath(`/pipelines/${id}`), { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Pipeline>(apiPath("/pipelines"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<Pipeline>(apiPath(`/pipelines/${id}`), {
|
||
tenantId,
|
||
method: "PATCH",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
stages: (tenantId: string, pipelineId: string, params?: ListParams) =>
|
||
request<PipelineStage[]>(
|
||
`${apiPath(`/pipelines/${pipelineId}/stages`)}${qs(params)}`,
|
||
{ tenantId }
|
||
),
|
||
createStage: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<PipelineStage>(apiPath("/pipelines/stages"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
updateStage: (tenantId: string, stageId: string, body: Record<string, unknown>) =>
|
||
request<PipelineStage>(apiPath(`/pipelines/stages/${stageId}`), {
|
||
tenantId,
|
||
method: "PATCH",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
getStage: (tenantId: string, stageId: string) =>
|
||
request<PipelineStage>(apiPath(`/pipelines/stages/${stageId}`), { tenantId }),
|
||
},
|
||
|
||
activities: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<SalesActivity[]>(`${apiPath("/activities")}${qs(params)}`, { tenantId }),
|
||
get: (tenantId: string, id: string) =>
|
||
request<SalesActivity>(apiPath(`/activities/${id}`), { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<SalesActivity>(apiPath("/activities"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<SalesActivity>(apiPath(`/activities/${id}`), {
|
||
tenantId,
|
||
method: "PATCH",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
complete: (tenantId: string, id: string) =>
|
||
request<SalesActivity>(apiPath(`/activities/${id}/complete`), {
|
||
tenantId,
|
||
method: "POST",
|
||
}),
|
||
},
|
||
|
||
quotes: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<Quote[]>(`${apiPath("/quotes")}${qs(params)}`, { tenantId }),
|
||
get: (tenantId: string, id: string) =>
|
||
request<Quote>(apiPath(`/quotes/${id}`), { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Quote>(apiPath("/quotes"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<Quote>(apiPath(`/quotes/${id}`), {
|
||
tenantId,
|
||
method: "PATCH",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
|
||
tags: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<Tag[]>(`${apiPath("/tags")}${qs(params)}`, { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Tag>(apiPath("/tags"), { tenantId, method: "POST", body: JSON.stringify(body) }),
|
||
assignLead: (tenantId: string, leadId: string, tagId: string) =>
|
||
request<Record<string, unknown>>(apiPath(`/tags/leads/${leadId}`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify({ tag_id: tagId }),
|
||
}),
|
||
assignContact: (tenantId: string, contactId: string, tagId: string) =>
|
||
request<Record<string, unknown>>(apiPath(`/tags/contacts/${contactId}`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify({ tag_id: tagId }),
|
||
}),
|
||
assignOrganization: (tenantId: string, orgId: string, tagId: string) =>
|
||
request<Record<string, unknown>>(apiPath(`/tags/organizations/${orgId}`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify({ tag_id: tagId }),
|
||
}),
|
||
},
|
||
|
||
notes: {
|
||
list: (tenantId: string, entityType: string, entityId: string) =>
|
||
request<Note[]>(
|
||
`${apiPath("/notes")}?entity_type=${entityType}&entity_id=${entityId}`,
|
||
{ tenantId }
|
||
),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Note>(apiPath("/notes"), { tenantId, method: "POST", body: JSON.stringify(body) }),
|
||
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<Note>(apiPath(`/notes/${id}`), {
|
||
tenantId,
|
||
method: "PATCH",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
|
||
attachments: {
|
||
list: (tenantId: string, entityType: string, entityId: string) =>
|
||
request<Attachment[]>(
|
||
`${apiPath("/attachments")}?entity_type=${entityType}&entity_id=${entityId}`,
|
||
{ tenantId }
|
||
),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Attachment>(apiPath("/attachments"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
|
||
customFields: {
|
||
list: (tenantId: string, entityType: string) =>
|
||
request<CustomField[]>(`${apiPath("/custom-fields")}?entity_type=${entityType}`, {
|
||
tenantId,
|
||
}),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<CustomField>(apiPath("/custom-fields"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
upsertValue: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Record<string, unknown>>(apiPath("/custom-fields/values"), {
|
||
tenantId,
|
||
method: "PUT",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
|
||
audit: {
|
||
list: (tenantId: string, entityType: string, entityId: string) =>
|
||
request<AuditEntry[]>(
|
||
`${apiPath("/audit")}?entity_type=${entityType}&entity_id=${entityId}`,
|
||
{ tenantId }
|
||
),
|
||
},
|
||
|
||
lookups: {
|
||
leadSources: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<LookupRead[]>(`${apiPath("/lookups/lead-sources")}${qs(params)}`, { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<LookupRead>(apiPath("/lookups/lead-sources"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
leadStatuses: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<LookupRead[]>(`${apiPath("/lookups/lead-statuses")}${qs(params)}`, { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<LookupRead>(apiPath("/lookups/lead-statuses"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
contactTypes: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<LookupRead[]>(`${apiPath("/lookups/contact-types")}${qs(params)}`, { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<LookupRead>(apiPath("/lookups/contact-types"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
organizationTypes: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<LookupRead[]>(
|
||
`${apiPath("/lookups/organization-types")}${qs(params)}`,
|
||
{ tenantId }
|
||
),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<LookupRead>(apiPath("/lookups/organization-types"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
organizationIndustries: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<LookupRead[]>(
|
||
`${apiPath("/lookups/organization-industries")}${qs(params)}`,
|
||
{ tenantId }
|
||
),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<LookupRead>(apiPath("/lookups/organization-industries"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
},
|
||
|
||
tasks: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<Task[]>(`${apiPath("/tasks")}${qs(params)}`, { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Task>(apiPath("/tasks"), { tenantId, method: "POST", body: JSON.stringify(body) }),
|
||
complete: (tenantId: string, id: string) =>
|
||
request<Task>(apiPath(`/tasks/${id}/complete`), { tenantId, method: "POST" }),
|
||
createChecklistItem: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Record<string, unknown>>(apiPath("/tasks/checklist"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
completeChecklistItem: (tenantId: string, itemId: string) =>
|
||
request<Record<string, unknown>>(apiPath(`/tasks/checklist/${itemId}/complete`), {
|
||
tenantId,
|
||
method: "POST",
|
||
}),
|
||
},
|
||
|
||
meetings: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<Meeting[]>(`${apiPath("/meetings")}${qs(params)}`, { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Meeting>(apiPath("/meetings"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
finish: (tenantId: string, id: string, body?: Record<string, unknown>) =>
|
||
request<Meeting>(apiPath(`/meetings/${id}/finish`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body ?? {}),
|
||
}),
|
||
addAttendee: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Record<string, unknown>>(apiPath("/meetings/attendees"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
|
||
calls: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<CallLog[]>(`${apiPath("/calls")}${qs(params)}`, { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<CallLog>(apiPath("/calls"), { tenantId, method: "POST", body: JSON.stringify(body) }),
|
||
},
|
||
|
||
emails: {
|
||
createThread: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Record<string, unknown>>(apiPath("/email-threads"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
createMessage: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Record<string, unknown>>(apiPath("/emails"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
|
||
timeline: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<TimelineEvent[]>(`${apiPath("/timeline")}${qs(params)}`, { tenantId }),
|
||
forOpportunity: (tenantId: string, opportunityId: string) =>
|
||
request<TimelineEvent[]>(apiPath(`/timeline/opportunities/${opportunityId}`), { tenantId }),
|
||
},
|
||
|
||
comments: {
|
||
list: (tenantId: string, entityType: string, entityId: string) =>
|
||
request<Comment[]>(
|
||
`${apiPath("/comments")}?entity_type=${entityType}&entity_id=${entityId}`,
|
||
{ tenantId }
|
||
),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Comment>(apiPath("/comments"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<Comment>(apiPath(`/comments/${id}`), {
|
||
tenantId,
|
||
method: "PATCH",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
|
||
mentions: {
|
||
unread: (tenantId: string) =>
|
||
request<Mention[]>(apiPath("/mentions/unread"), { tenantId }),
|
||
markRead: (tenantId: string, id: string) =>
|
||
request<Mention>(apiPath(`/mentions/${id}/read`), { tenantId, method: "POST" }),
|
||
},
|
||
|
||
team: {
|
||
members: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<TeamMember[]>(`${apiPath("/team-members")}${qs(params)}`, { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<TeamMember>(apiPath("/team-members"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
roles: {
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Record<string, unknown>>(apiPath("/team-roles"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
assignments: {
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Record<string, unknown>>(apiPath("/team-assignments"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
},
|
||
|
||
bookmarks: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<Record<string, unknown>[]>(`${apiPath("/bookmarks")}${qs(params)}`, { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Record<string, unknown>>(apiPath("/bookmarks"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
|
||
favorites: {
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Record<string, unknown>>(apiPath("/favorites"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
|
||
playbooks: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<Playbook[]>(`${apiPath("/playbooks")}${qs(params)}`, { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Playbook>(apiPath("/playbooks"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
steps: (tenantId: string, playbookId: string) =>
|
||
request<Record<string, unknown>[]>(apiPath(`/playbooks/${playbookId}/steps`), { tenantId }),
|
||
createStep: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Record<string, unknown>>(apiPath("/playbooks/steps"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
assign: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Record<string, unknown>>(apiPath("/playbooks/assignments"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
completeStep: (tenantId: string, assignmentId: string, body: Record<string, unknown>) =>
|
||
request<Record<string, unknown>>(
|
||
apiPath(`/playbooks/assignments/${assignmentId}/complete-step`),
|
||
{ tenantId, method: "POST", body: JSON.stringify(body) }
|
||
),
|
||
},
|
||
|
||
forecasts: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<Forecast[]>(`${apiPath("/forecasts")}${qs(params)}`, { tenantId }),
|
||
compute: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Forecast>(apiPath("/forecasts/compute"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
|
||
goals: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<Goal[]>(`${apiPath("/goals")}${qs(params)}`, { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Goal>(apiPath("/goals"), { tenantId, method: "POST", body: JSON.stringify(body) }),
|
||
},
|
||
|
||
targets: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<Target[]>(`${apiPath("/targets")}${qs(params)}`, { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Target>(apiPath("/targets"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
|
||
salesProcesses: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<Record<string, unknown>[]>(`${apiPath("/sales-processes")}${qs(params)}`, {
|
||
tenantId,
|
||
}),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Record<string, unknown>>(apiPath("/sales-processes"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
|
||
winLoss: {
|
||
categories: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<Record<string, unknown>[]>(
|
||
`${apiPath("/win-loss/categories")}${qs(params)}`,
|
||
{ tenantId }
|
||
),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Record<string, unknown>>(apiPath("/win-loss/categories"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
reasons: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<Record<string, unknown>[]>(
|
||
`${apiPath("/win-loss/reasons")}${qs(params)}`,
|
||
{ tenantId }
|
||
),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Record<string, unknown>>(apiPath("/win-loss/reasons"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
forOpportunity: (tenantId: string, opportunityId: string) =>
|
||
request<Record<string, unknown>>(
|
||
apiPath(`/win-loss/opportunities/${opportunityId}`),
|
||
{ tenantId }
|
||
),
|
||
},
|
||
|
||
collaborationPreferences: {
|
||
upsert: (tenantId: string, userId: string, body: Record<string, unknown>) =>
|
||
request<Record<string, unknown>>(apiPath(`/collaboration-preferences/${userId}`), {
|
||
tenantId,
|
||
method: "PUT",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
};
|
||
|
||
/** Fetch all pages client-side (backend has no total count). */
|
||
export async function crmFetchAll<T>(
|
||
fetchPage: (params: ListParams) => Promise<T[]>,
|
||
pageSize = 500
|
||
): Promise<T[]> {
|
||
const all: T[] = [];
|
||
let page = 1;
|
||
for (;;) {
|
||
const batch = await fetchPage({ page, page_size: pageSize });
|
||
all.push(...batch);
|
||
if (batch.length < pageSize) break;
|
||
page += 1;
|
||
if (page > 50) break;
|
||
}
|
||
return all;
|
||
}
|