La URL prefirmada usaba el host interno http://minio:9000 (no accesible desde el navegador). Se agrega GET /quotes/{id}/pdf que devuelve el PDF por el backend (vía nginx) y el visor usa un blob autenticado (api.getBlob). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
205 lines
9.7 KiB
TypeScript
205 lines
9.7 KiB
TypeScript
/**
|
|
* Cliente API — Proceso comercial (Solicitudes/RFQ, tarifas, Cotizaciones).
|
|
*/
|
|
import { api } from '$lib/api';
|
|
|
|
// ---------- Tipos ----------
|
|
export type ServiceRequestStatus = 'nueva' | 'contacto' | 'en_analisis' | 'cotizada' | 'aceptada' | 'rechazada' | 'liberada';
|
|
export type QuoteStatus = 'borrador' | 'enviada' | 'aceptada' | 'rechazada';
|
|
|
|
export interface ServiceRequest {
|
|
id: number;
|
|
reference: string | null;
|
|
account_id: number | null;
|
|
opportunity_id: number | null;
|
|
operation_type: string;
|
|
transport_mode: string | null;
|
|
service_type: string | null;
|
|
incoterm: string | null;
|
|
origin: string | null;
|
|
destination: string | null;
|
|
cargo_type: string | null;
|
|
weight: number | null;
|
|
volume: number | null;
|
|
load_type: string | null;
|
|
container_equipment: string | null;
|
|
commodity: string | null;
|
|
required_date: string | null;
|
|
destination_agent_id: number | null;
|
|
requirements: string | null;
|
|
first_contact_at: string | null;
|
|
first_contact_notes: string | null;
|
|
status: ServiceRequestStatus;
|
|
notes: string | null;
|
|
owner_user_id: string | null;
|
|
created_by: string | null;
|
|
updated_by: string | null;
|
|
tenant_id: number;
|
|
company_id: number;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
export type ServiceRequestInput = Partial<Omit<ServiceRequest, 'id' | 'tenant_id' | 'company_id' | 'created_at' | 'updated_at' | 'created_by' | 'updated_by'>> & {
|
|
operation_type: string;
|
|
};
|
|
|
|
export interface RateRequest {
|
|
id: number;
|
|
service_request_id: number;
|
|
supplier_id: number | null;
|
|
concept: string;
|
|
description: string | null;
|
|
status: string;
|
|
rate_amount: number | null;
|
|
currency: string | null;
|
|
valid_until: string | null;
|
|
notes: string | null;
|
|
tenant_id: number;
|
|
company_id: number;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
export type RateRequestInput = Partial<Omit<RateRequest, 'id' | 'tenant_id' | 'company_id' | 'created_at' | 'updated_at'>> & {
|
|
service_request_id: number;
|
|
concept: string;
|
|
};
|
|
|
|
export interface Quote {
|
|
id: number;
|
|
reference: string | null;
|
|
service_request_id: number | null;
|
|
account_id: number | null;
|
|
currency: string;
|
|
status: QuoteStatus;
|
|
issue_date: string | null;
|
|
valid_until: string | null;
|
|
total_cost: number;
|
|
total_sale: number;
|
|
margin: number;
|
|
sent_at: string | null;
|
|
accepted_at: string | null;
|
|
rejected_at: string | null;
|
|
notes: string | null;
|
|
terms: string | null;
|
|
owner_user_id: string | null;
|
|
created_by: string | null;
|
|
updated_by: string | null;
|
|
tenant_id: number;
|
|
company_id: number;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
export type QuoteInput = Partial<Omit<Quote, 'id' | 'status' | 'total_cost' | 'total_sale' | 'margin' | 'sent_at' | 'accepted_at' | 'rejected_at' | 'tenant_id' | 'company_id' | 'created_at' | 'updated_at' | 'created_by' | 'updated_by'>>;
|
|
|
|
export interface QuoteItem {
|
|
id: number;
|
|
quote_id: number;
|
|
concept: string;
|
|
description: string | null;
|
|
supplier_id: number | null;
|
|
quantity: number;
|
|
unit_cost: number;
|
|
unit_sale: number;
|
|
currency: string | null;
|
|
line_cost: number;
|
|
line_sale: number;
|
|
tenant_id: number;
|
|
company_id: number;
|
|
}
|
|
export type QuoteItemInput = Partial<Omit<QuoteItem, 'id' | 'line_cost' | 'line_sale' | 'tenant_id' | 'company_id'>> & {
|
|
quote_id: number;
|
|
concept: string;
|
|
};
|
|
|
|
// ---------- Clientes ----------
|
|
function qp(companyId: number, extra?: Record<string, string | number | undefined>) {
|
|
const qs = new URLSearchParams({ company_id: String(companyId) });
|
|
for (const [k, v] of Object.entries(extra ?? {})) if (v !== undefined && v !== '') qs.set(k, String(v));
|
|
return qs.toString();
|
|
}
|
|
async function unwrap<T>(p: Promise<{ data?: T; error?: string }>): Promise<T> {
|
|
const res = await p;
|
|
if (res.error) throw new Error(res.error);
|
|
return res.data as T;
|
|
}
|
|
|
|
export const serviceRequestsAPI = {
|
|
list: (companyId: number, params?: { search?: string; status?: string; operation_type?: string; account_id?: number }) =>
|
|
unwrap<ServiceRequest[]>(api.get(`/v1/crm/service-requests?${qp(companyId, params)}`)),
|
|
get: (id: number, companyId: number) => unwrap<ServiceRequest>(api.get(`/v1/crm/service-requests/${id}?${qp(companyId)}`)),
|
|
create: (data: ServiceRequestInput, companyId: number) => unwrap<ServiceRequest>(api.post(`/v1/crm/service-requests?${qp(companyId)}`, data)),
|
|
update: (id: number, data: Partial<ServiceRequestInput>, companyId: number) => unwrap<ServiceRequest>(api.patch(`/v1/crm/service-requests/${id}?${qp(companyId)}`, data)),
|
|
registerContact: (id: number, companyId: number, notes?: string | null) =>
|
|
unwrap<ServiceRequest>(api.post(`/v1/crm/service-requests/${id}/contact?${qp(companyId)}`, { notes })),
|
|
requote: (id: number, companyId: number) =>
|
|
unwrap<ServiceRequest>(api.post(`/v1/crm/service-requests/${id}/requote?${qp(companyId)}`, {})),
|
|
fromOpportunity: (opportunityId: number, data: { operation_type: string; transport_mode?: string; service_type?: string; incoterm?: string; origin?: string; destination?: string; notes?: string | null }, companyId: number) =>
|
|
unwrap<ServiceRequest>(api.post(`/v1/crm/service-requests/from-opportunity?${qp(companyId, { opportunity_id: opportunityId })}`, data)),
|
|
remove: (id: number, companyId: number) => unwrap(api.delete(`/v1/crm/service-requests/${id}?${qp(companyId)}`))
|
|
};
|
|
|
|
export const rateRequestsAPI = {
|
|
list: (companyId: number, serviceRequestId?: number) =>
|
|
unwrap<RateRequest[]>(api.get(`/v1/crm/rate-requests?${qp(companyId, { service_request_id: serviceRequestId })}`)),
|
|
create: (data: RateRequestInput, companyId: number) => unwrap<RateRequest>(api.post(`/v1/crm/rate-requests?${qp(companyId)}`, data)),
|
|
update: (id: number, data: Partial<RateRequestInput>, companyId: number) => unwrap<RateRequest>(api.patch(`/v1/crm/rate-requests/${id}?${qp(companyId)}`, data)),
|
|
remove: (id: number, companyId: number) => unwrap(api.delete(`/v1/crm/rate-requests/${id}?${qp(companyId)}`))
|
|
};
|
|
|
|
export const quotesAPI = {
|
|
list: (companyId: number, params?: { search?: string; status?: string; account_id?: number }) =>
|
|
unwrap<Quote[]>(api.get(`/v1/crm/quotes?${qp(companyId, params)}`)),
|
|
get: (id: number, companyId: number) => unwrap<Quote>(api.get(`/v1/crm/quotes/${id}?${qp(companyId)}`)),
|
|
create: (data: QuoteInput, companyId: number) => unwrap<Quote>(api.post(`/v1/crm/quotes?${qp(companyId)}`, data)),
|
|
update: (id: number, data: Partial<QuoteInput>, companyId: number) => unwrap<Quote>(api.patch(`/v1/crm/quotes/${id}?${qp(companyId)}`, data)),
|
|
send: (id: number, companyId: number) => unwrap<Quote>(api.patch(`/v1/crm/quotes/${id}/send?${qp(companyId)}`, {})),
|
|
accept: (id: number, companyId: number) => unwrap<Quote>(api.patch(`/v1/crm/quotes/${id}/accept?${qp(companyId)}`, {})),
|
|
reject: (id: number, companyId: number) => unwrap<Quote>(api.patch(`/v1/crm/quotes/${id}/reject?${qp(companyId)}`, {})),
|
|
clone: (id: number, companyId: number) => unwrap<Quote>(api.post(`/v1/crm/quotes/${id}/clone?${qp(companyId)}`, {})),
|
|
remove: (id: number, companyId: number) => unwrap(api.delete(`/v1/crm/quotes/${id}?${qp(companyId)}`)),
|
|
items: (quoteId: number, companyId: number) => unwrap<QuoteItem[]>(api.get(`/v1/crm/quotes/${quoteId}/items?${qp(companyId)}`)),
|
|
pdfBlob: (id: number, companyId: number) => (api as any).getBlob(`/v1/crm/quotes/${id}/pdf?${qp(companyId)}`) as Promise<Blob>,
|
|
sendEmail: (id: number, companyId: number, body: { to?: string | null; subject?: string | null; message?: string | null }) =>
|
|
unwrap<{ sent_to: string; reference: string }>(api.post(`/v1/crm/quotes/${id}/send-email?${qp(companyId)}`, body))
|
|
};
|
|
|
|
// ---------- Configuración de marca del formato de cotización ----------
|
|
export interface QuoteSettings {
|
|
id?: number | null;
|
|
emitter_name?: string | null; emitter_rfc?: string | null; emitter_address?: string | null;
|
|
emitter_phone?: string | null; emitter_email?: string | null; emitter_website?: string | null;
|
|
logo_file_key?: string | null; accent_color?: string | null; quote_prefix?: string | null;
|
|
default_terms?: string | null; footer_note?: string | null;
|
|
}
|
|
|
|
export const quoteSettingsAPI = {
|
|
get: (companyId: number) => unwrap<QuoteSettings>(api.get(`/v1/crm/quote-settings?${qp(companyId)}`)),
|
|
save: (companyId: number, data: QuoteSettings) => unwrap<QuoteSettings>(api.put(`/v1/crm/quote-settings?${qp(companyId)}`, data)),
|
|
logoUrl: (companyId: number) => unwrap<{ url: string | null }>(api.get(`/v1/crm/quote-settings/logo-url?${qp(companyId)}`)),
|
|
async uploadLogo(companyId: number, file: File): Promise<QuoteSettings> {
|
|
const fd = new FormData();
|
|
fd.append('file', file);
|
|
const res = await (api as any).request(`/v1/crm/quote-settings/logo?${qp(companyId)}`, { method: 'POST', body: fd });
|
|
if (res.error) throw new Error(res.error);
|
|
return res.data as QuoteSettings;
|
|
}
|
|
};
|
|
|
|
// ---------- Catálogos de referencia (Incoterms, participantes) ----------
|
|
export interface Incoterm { code: string; name: string; }
|
|
export interface ParticipantRole { code: string; label: string; source: string; }
|
|
export interface Participant { id: number; source: string; name: string; role: string; roles: string[]; }
|
|
|
|
export const catalogsAPI = {
|
|
incoterms: (companyId: number) => unwrap<Incoterm[]>(api.get(`/v1/crm/catalogs/incoterms?${qp(companyId)}`)),
|
|
participantRoles: (companyId: number) => unwrap<ParticipantRole[]>(api.get(`/v1/crm/catalogs/participant-roles?${qp(companyId)}`)),
|
|
participants: (companyId: number, role?: string) =>
|
|
unwrap<Participant[]>(api.get(`/v1/crm/participants?${qp(companyId, { role })}`))
|
|
};
|
|
|
|
export const quoteItemsAPI = {
|
|
create: (data: QuoteItemInput, companyId: number) => unwrap<QuoteItem>(api.post(`/v1/crm/quote-items?${qp(companyId)}`, data)),
|
|
update: (id: number, data: Partial<QuoteItemInput>, companyId: number) => unwrap<QuoteItem>(api.patch(`/v1/crm/quote-items/${id}?${qp(companyId)}`, data)),
|
|
remove: (id: number, companyId: number) => unwrap(api.delete(`/v1/crm/quote-items/${id}?${qp(companyId)}`))
|
|
};
|