feat(crm): frontend del módulo Tarifario — tarifarios, import Excel y Cotizador
- Cliente API rateSheetsAPI (CRUD, plantilla, import con vista previa, /rate-quote). - Tarifarios: listado + importar por Excel (plantilla, vista previa, validación) + detalle con rutas y activar/vencer. - Cotizador: calcula opciones de costo por proveedor desde los tarifarios vigentes. - Nav CRM: Tarifarios y Cotizador. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
98
frontend/src/lib/api/crm/rates.ts
Normal file
98
frontend/src/lib/api/crm/rates.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Cliente API — Módulo Tarifario (tarifarios, rutas, import Excel, costeo).
|
||||
*/
|
||||
import { api } from '$lib/api';
|
||||
|
||||
export type RateMode = 'aereo' | 'maritimo_fcl' | 'maritimo_lcl' | 'terrestre';
|
||||
|
||||
export interface RateBreak { from_qty: number; rate: number; }
|
||||
export interface RateLane {
|
||||
id: number; rate_sheet_id: number;
|
||||
origin: string | null; destination: string | null; region: string | null;
|
||||
equipment_type: string | null; rate_unit: string | null;
|
||||
min_charge: number | null; flat_rate: number | null; transit_days: number | null; notes: string | null;
|
||||
breaks: RateBreak[];
|
||||
}
|
||||
export interface RateSheet {
|
||||
id: number; supplier_id: number | null; mode: RateMode; name: string;
|
||||
currency: string | null; valid_from: string | null; valid_to: string | null;
|
||||
default_origin: string | null; status: string; notes: string | null;
|
||||
source_file: string | null; created_by: string | null; updated_by: string | null;
|
||||
created_at: string; updated_at: string; lane_count: number | null;
|
||||
}
|
||||
export type RateSheetInput = Partial<Omit<RateSheet, 'id' | 'created_at' | 'updated_at' | 'lane_count' | 'source_file' | 'created_by' | 'updated_by'>> & {
|
||||
mode: RateMode; name: string;
|
||||
};
|
||||
|
||||
export interface ImportPreviewRow { row: number; data: Record<string, unknown>; ok: boolean; warnings: string[]; errors: string[]; }
|
||||
export interface ImportPreview { mode: RateMode; total: number; valid: number; rows: ImportPreviewRow[]; columns: string[]; }
|
||||
|
||||
export interface CostRequest {
|
||||
mode: RateMode; origin?: string | null; destination?: string | null; on_date?: string | null;
|
||||
gross_weight_kg?: number | null; volume_m3?: number | null; equipment_type?: string | null;
|
||||
quantity?: number; dangerous?: boolean;
|
||||
}
|
||||
export interface CostChargeLine { concept: string; amount: number; }
|
||||
export interface CostOption {
|
||||
rate_sheet_id: number; rate_sheet_name: string; supplier_id: number | null; currency: string | null;
|
||||
chargeable: number | null; base_cost: number; charges: CostChargeLine[]; total_cost: number;
|
||||
transit_days: number | null; detail: string | null;
|
||||
}
|
||||
export interface CostResult { request: CostRequest; options: CostOption[]; }
|
||||
|
||||
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 rateSheetsAPI = {
|
||||
list: (companyId: number, params?: { mode?: string; supplier_id?: number }) =>
|
||||
unwrap<RateSheet[]>(api.get(`/v1/crm/rate-sheets?${qp(companyId, params)}`)),
|
||||
get: (id: number, companyId: number) => unwrap<RateSheet>(api.get(`/v1/crm/rate-sheets/${id}?${qp(companyId)}`)),
|
||||
create: (data: RateSheetInput, companyId: number) => unwrap<RateSheet>(api.post(`/v1/crm/rate-sheets?${qp(companyId)}`, data)),
|
||||
update: (id: number, data: Partial<RateSheetInput>, companyId: number) => unwrap<RateSheet>(api.patch(`/v1/crm/rate-sheets/${id}?${qp(companyId)}`, data)),
|
||||
remove: (id: number, companyId: number) => unwrap(api.delete(`/v1/crm/rate-sheets/${id}?${qp(companyId)}`)),
|
||||
lanes: (id: number, companyId: number) => unwrap<RateLane[]>(api.get(`/v1/crm/rate-sheets/${id}/lanes?${qp(companyId)}`)),
|
||||
addLane: (id: number, data: Partial<RateLane>, companyId: number) => unwrap<RateLane>(api.post(`/v1/crm/rate-sheets/${id}/lanes?${qp(companyId)}`, data)),
|
||||
removeLane: (id: number, laneId: number, companyId: number) => unwrap(api.delete(`/v1/crm/rate-sheets/${id}/lanes/${laneId}?${qp(companyId)}`)),
|
||||
|
||||
/** Descarga la plantilla Excel del modo. */
|
||||
async downloadTemplate(mode: RateMode, companyId: number): Promise<void> {
|
||||
const blob = await api.getBlob(`/v1/crm/rate-sheets/template?${qp(companyId, { mode })}`);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url; a.download = `plantilla_tarifario_${mode}.xlsx`;
|
||||
document.body.appendChild(a); a.click(); a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
|
||||
async importPreview(mode: RateMode, file: File, companyId: number): Promise<ImportPreview> {
|
||||
const fd = new FormData();
|
||||
fd.append('mode', mode); fd.append('file', file);
|
||||
const res = await (api as any).request(`/v1/crm/rate-sheets/import/preview?${qp(companyId)}`, { method: 'POST', body: fd });
|
||||
if (res.error) throw new Error(res.error);
|
||||
return res.data as ImportPreview;
|
||||
},
|
||||
|
||||
async importSheet(companyId: number, header: { mode: RateMode; name: string; supplier_id?: number | null; currency?: string; valid_from?: string | null; valid_to?: string | null; default_origin?: string | null }, file: File): Promise<RateSheet> {
|
||||
const fd = new FormData();
|
||||
fd.append('mode', header.mode); fd.append('name', header.name);
|
||||
if (header.supplier_id != null) fd.append('supplier_id', String(header.supplier_id));
|
||||
if (header.currency) fd.append('currency', header.currency);
|
||||
if (header.valid_from) fd.append('valid_from', header.valid_from);
|
||||
if (header.valid_to) fd.append('valid_to', header.valid_to);
|
||||
if (header.default_origin) fd.append('default_origin', header.default_origin);
|
||||
fd.append('file', file);
|
||||
const res = await (api as any).request(`/v1/crm/rate-sheets/import?${qp(companyId)}`, { method: 'POST', body: fd });
|
||||
if (res.error) throw new Error(res.error);
|
||||
return res.data as RateSheet;
|
||||
},
|
||||
|
||||
quote: (req: CostRequest, companyId: number) => unwrap<CostResult>(api.post(`/v1/crm/rate-quote?${qp(companyId)}`, req))
|
||||
};
|
||||
Reference in New Issue
Block a user