import api from './client';
// ─── Types ────────────────────────────────────────────────────────────────
export type EstimateStage = 'draft' | 'delivered' | 'accepted' | 'rejected' | 'expired' | 'scheduled' | 'closed';
export type EstimateSource = 'manual' | 'angi' | 'servicetitan' | 'jobber' | 'hubspot' | 'email' | 'referral' | 'website';
export interface StageHistoryEntry {
from_stage: EstimateStage | null;
to_stage?: EstimateStage;
stage?: EstimateStage;
timestamp: string;
notes?: string;
changed_by?: string;
}
export interface LineItem {
description: string;
quantity: number;
unit_price: number;
total: number;
}
export interface Estimate {
id: string;
company_id: string;
estimate_number: string;
project_id?: string;
customer_id?: string;
source_lead_id?: string;
customer_name: string;
customer_email: string;
customer_phone: string;
project_type: string;
description: string;
address: string;
city: string;
state: string;
zip_code: string;
total_value: number;
deposit: number;
tax_amount: number;
discount_amount: number;
line_items: LineItem[];
stage: EstimateStage;
stage_history: StageHistoryEntry[];
days_in_stage: number | null;
current_stage_timestamp: string | null;
expires_at: string | null;
rejection_reason: string;
competitor_name: string;
competitor_price: number | null;
notes: string;
assigned_to: string | null;
source: EstimateSource;
source_estimate_id: string;
is_active: boolean;
is_closed: boolean;
delivered_at: string | null;
accepted_at: string | null;
rejected_at: string | null;
scheduled_date: string | null;
closed_at: string | null;
created_at: string;
updated_at: string;
}
// ─── Funnel Analytics ─────────────────────────────────────────────────────
export interface ConversionRate {
from_stage: EstimateStage;
to_stage: EstimateStage;
from_count: number;
to_count: number;
rate: number;
}
export interface PipelineSummary {
total_pipeline_value: number;
closed_value: number;
closed_count: number;
win_rate: number;
avg_deal_size: number;
avg_time_to_accept_days: number | null;
}
export interface DropOffData {
rejected_count: number;
expired_count: number;
total_drop_off_count: number;
drop_off_rate: number;
rejection_reasons: Record<string, number>;
}
export interface FunnelAnalytics {
period_days: number;
total_estimates: number;
stage_counts: Record<EstimateStage, number>;
stage_values: Record<EstimateStage, number>;
conversion_rates: Record<string, ConversionRate>;
pipeline_summary: PipelineSummary;
drop_off: DropOffData;
}
export interface PipelineStageData {
count: number;
value: number;
avg_value: number;
}
export interface PipelineOverview {
period_days: number;
total_estimates: number;
active_estimates: number;
closed_estimates: number;
pipeline: Record<string, PipelineStageData>;
total_pipeline_value: number;
total_closed_value: number;
}
export interface EstimateStats {
period_days: number;
total_count: number;
active_count: number;
closed_count: number;
total_value: number;
pipeline_value: number;
closed_value: number;
avg_estimate_size: number;
conversion_rate: number;
avg_days_to_close: number | null;
}
export interface SourceBreakdown {
count: number;
total_value: number;
closed_count: number;
closed_value: number;
conversion_rate: number;
avg_value: number;
}
export interface EstimatesBySource {
period_days: number;
sources: Record<string, SourceBreakdown>;
}
export interface EstimateListResponse {
estimates: Estimate[];
page: number;
per_page: number;
total: number;
total_pages: number;
pages?: number;
has_next?: boolean;
has_prev?: boolean;
}
// ─── API ──────────────────────────────────────────────────────────────────
export const estimatesApi = {
// CRUD
getEstimates: async (
companyId: string,
params?: {
stage?: EstimateStage;
source?: EstimateSource;
min_value?: number;
max_value?: number;
project_type?: string;
is_active?: string;
is_closed?: string;
sort_by?: string;
sort_order?: 'asc' | 'desc';
page?: number;
per_page?: number;
}
): Promise<EstimateListResponse> => {
const { data } = await api.get<EstimateListResponse>(
`/api/company/${companyId}/estimates`,
{ params }
);
return data;
},
getEstimate: async (companyId: string, estimateId: string): Promise<{ estimate: Estimate }> => {
const { data } = await api.get<{ estimate: Estimate }>(
`/api/company/${companyId}/estimates/${estimateId}`
);
return data;
},
createEstimate: async (companyId: string, payload: Partial<Estimate> & { total_value: number }): Promise<{ success: boolean; estimate: Estimate }> => {
const { data } = await api.post<{ success: boolean; estimate: Estimate }>(
`/api/company/${companyId}/estimates`,
payload
);
return data;
},
updateEstimate: async (companyId: string, estimateId: string, payload: Partial<Estimate>): Promise<{ success: boolean; estimate: Estimate }> => {
const { data } = await api.put<{ success: boolean; estimate: Estimate }>(
`/api/company/${companyId}/estimates/${estimateId}`,
payload
);
return data;
},
deleteEstimate: async (companyId: string, estimateId: string): Promise<{ success: boolean; message: string }> => {
const { data } = await api.delete<{ success: boolean; message: string }>(
`/api/company/${companyId}/estimates/${estimateId}`
);
return data;
},
// Transitions
transitionEstimate: async (
companyId: string,
estimateId: string,
payload: { stage: EstimateStage; notes?: string; scheduled_date?: string }
): Promise<{ success: boolean; estimate: Estimate; transition: { from_stage: string; to_stage: string; timestamp: string } }> => {
const { data } = await api.patch<{ success: boolean; estimate: Estimate; transition: { from_stage: string; to_stage: string; timestamp: string } }>(
`/api/company/${companyId}/estimates/${estimateId}/transition`,
payload
);
return data;
},
getStageHistory: async (companyId: string, estimateId: string): Promise<{ estimate_id: string; current_stage: EstimateStage; stage_history: StageHistoryEntry[] }> => {
const { data } = await api.get<{ estimate_id: string; current_stage: EstimateStage; stage_history: StageHistoryEntry[] }>(
`/api/company/${companyId}/estimates/${estimateId}/stage-history`
);
return data;
},
// Bulk
expireOldEstimates: async (
companyId: string,
daysBeforeExpiry?: number
): Promise<{ success: boolean; expired_count: number; expired_ids: string[] }> => {
const { data } = await api.patch<{ success: boolean; expired_count: number; expired_ids: string[] }>(
`/api/company/${companyId}/estimates/expired`,
{},
{ params: daysBeforeExpiry ? { days_before_expiry: daysBeforeExpiry } : {} }
);
return data;
},
// Analytics
getFunnelAnalytics: async (
companyId: string,
params?: { days?: number; source?: EstimateSource; project_type?: string }
): Promise<FunnelAnalytics> => {
const { data } = await api.get<FunnelAnalytics>(
`/api/company/${companyId}/estimates/funnel`,
{ params }
);
return data;
},
getPipelineOverview: async (
companyId: string,
days?: number
): Promise<PipelineOverview> => {
const { data } = await api.get<PipelineOverview>(
`/api/company/${companyId}/estimates/pipeline`,
{ params: { days: days ?? 90 } }
);
return data;
},
getEstimateStats: async (
companyId: string,
days?: number
): Promise<EstimateStats> => {
const { data } = await api.get<EstimateStats>(
`/api/company/${companyId}/estimates/stats`,
{ params: { days: days ?? 30 } }
);
return data;
},
getEstimatesBySource: async (
companyId: string,
days?: number
): Promise<EstimatesBySource> => {
const { data } = await api.get<EstimatesBySource>(
`/api/company/${companyId}/estimates/by-source`,
{ params: { days: days ?? 90 } }
);
return data;
},
};