diff --git a/frontend/src/lib/api/crm/commercial.ts b/frontend/src/lib/api/crm/commercial.ts new file mode 100644 index 0000000..a762782 --- /dev/null +++ b/frontend/src/lib/api/crm/commercial.ts @@ -0,0 +1,157 @@ +/** + * Cliente API — Proceso comercial (Solicitudes/RFQ, tarifas, Cotizaciones). + */ +import { api } from '$lib/api'; + +// ---------- Tipos ---------- +export type ServiceRequestStatus = 'nueva' | '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; + 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; + 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> & { + 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> & { + 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>; + +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> & { + quote_id: number; + concept: string; +}; + +// ---------- Clientes ---------- +function qp(companyId: number, extra?: Record) { + 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(p: Promise<{ data?: T; error?: string }>): Promise { + 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(api.get(`/v1/crm/service-requests?${qp(companyId, params)}`)), + get: (id: number, companyId: number) => unwrap(api.get(`/v1/crm/service-requests/${id}?${qp(companyId)}`)), + create: (data: ServiceRequestInput, companyId: number) => unwrap(api.post(`/v1/crm/service-requests?${qp(companyId)}`, data)), + update: (id: number, data: Partial, companyId: number) => unwrap(api.patch(`/v1/crm/service-requests/${id}?${qp(companyId)}`, 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(api.get(`/v1/crm/rate-requests?${qp(companyId, { service_request_id: serviceRequestId })}`)), + create: (data: RateRequestInput, companyId: number) => unwrap(api.post(`/v1/crm/rate-requests?${qp(companyId)}`, data)), + update: (id: number, data: Partial, companyId: number) => unwrap(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(api.get(`/v1/crm/quotes?${qp(companyId, params)}`)), + get: (id: number, companyId: number) => unwrap(api.get(`/v1/crm/quotes/${id}?${qp(companyId)}`)), + create: (data: QuoteInput, companyId: number) => unwrap(api.post(`/v1/crm/quotes?${qp(companyId)}`, data)), + update: (id: number, data: Partial, companyId: number) => unwrap(api.patch(`/v1/crm/quotes/${id}?${qp(companyId)}`, data)), + send: (id: number, companyId: number) => unwrap(api.patch(`/v1/crm/quotes/${id}/send?${qp(companyId)}`, {})), + accept: (id: number, companyId: number) => unwrap(api.patch(`/v1/crm/quotes/${id}/accept?${qp(companyId)}`, {})), + reject: (id: number, companyId: number) => unwrap(api.patch(`/v1/crm/quotes/${id}/reject?${qp(companyId)}`, {})), + remove: (id: number, companyId: number) => unwrap(api.delete(`/v1/crm/quotes/${id}?${qp(companyId)}`)), + items: (quoteId: number, companyId: number) => unwrap(api.get(`/v1/crm/quotes/${quoteId}/items?${qp(companyId)}`)) +}; + +export const quoteItemsAPI = { + create: (data: QuoteItemInput, companyId: number) => unwrap(api.post(`/v1/crm/quote-items?${qp(companyId)}`, data)), + update: (id: number, data: Partial, companyId: number) => unwrap(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)}`)) +}; diff --git a/frontend/src/lib/api/crm/index.ts b/frontend/src/lib/api/crm/index.ts index 7949951..a2e40f9 100644 --- a/frontend/src/lib/api/crm/index.ts +++ b/frontend/src/lib/api/crm/index.ts @@ -12,3 +12,4 @@ export { pipelinesAPI, stagesAPI, type StageInput } from './pipelines'; export { opportunitiesAPI } from './opportunities'; export { activitiesAPI } from './activities'; export { metricsAPI } from './metrics'; +export * from './commercial'; diff --git a/frontend/src/lib/api/ops/index.ts b/frontend/src/lib/api/ops/index.ts new file mode 100644 index 0000000..9ecabb2 --- /dev/null +++ b/frontend/src/lib/api/ops/index.ts @@ -0,0 +1,90 @@ +/** + * Cliente API — Operaciones (Embarques y documentos de transporte). + */ +import { api } from '$lib/api'; + +export type ShipmentStatus = + | 'abierta' | 'booking' | 'en_transito' | 'arribado' | 'entregada' | 'cerrada' | 'cancelada'; + +export interface Shipment { + id: number; + reference: string | null; + quote_id: number | null; + service_request_id: number | null; + account_id: number | null; + operation_type: string | null; + transport_mode: string | null; + service_type: string | null; + incoterm: string | null; + origin: string | null; + destination: string | null; + status: ShipmentStatus; + booking_number: string | null; + carrier_supplier_id: number | null; + customs_agent_id: number | null; + destination_agent_id: number | null; + cutoff_date: string | null; + etd: string | null; + eta: string | null; + vessel_flight: string | null; + container_number: string | null; + 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 ShipmentInput = Partial>; + +export interface ShipmentDocument { + id: number; + shipment_id: number; + doc_kind: string; + doc_type: string; + number: string | null; + issue_date: string | null; + file_url: string | null; + file_key: string | null; + notes: string | null; + tenant_id: number; + company_id: number; + created_at: string; + updated_at: string; +} +export type ShipmentDocumentInput = Partial> & { + shipment_id: number; + doc_type: string; +}; + +function qp(companyId: number, extra?: Record) { + 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(p: Promise<{ data?: T; error?: string }>): Promise { + const res = await p; + if (res.error) throw new Error(res.error); + return res.data as T; +} + +export const shipmentsAPI = { + list: (companyId: number, params?: { search?: string; status?: string; account_id?: number }) => + unwrap(api.get(`/v1/ops/shipments?${qp(companyId, params)}`)), + get: (id: number, companyId: number) => unwrap(api.get(`/v1/ops/shipments/${id}?${qp(companyId)}`)), + create: (data: ShipmentInput, companyId: number) => unwrap(api.post(`/v1/ops/shipments?${qp(companyId)}`, data)), + createFromQuote: (quoteId: number, companyId: number) => + unwrap(api.post(`/v1/ops/shipments/from-quote?${qp(companyId, { quote_id: quoteId })}`, {})), + update: (id: number, data: Partial, companyId: number) => unwrap(api.patch(`/v1/ops/shipments/${id}?${qp(companyId)}`, data)), + remove: (id: number, companyId: number) => unwrap(api.delete(`/v1/ops/shipments/${id}?${qp(companyId)}`)), + documents: (shipmentId: number, companyId: number) => + unwrap(api.get(`/v1/ops/shipments/${shipmentId}/documents?${qp(companyId)}`)) +}; + +export const shipmentDocumentsAPI = { + create: (data: ShipmentDocumentInput, companyId: number) => unwrap(api.post(`/v1/ops/shipment-documents?${qp(companyId)}`, data)), + update: (id: number, data: Partial, companyId: number) => unwrap(api.patch(`/v1/ops/shipment-documents/${id}?${qp(companyId)}`, data)), + remove: (id: number, companyId: number) => unwrap(api.delete(`/v1/ops/shipment-documents/${id}?${qp(companyId)}`)) +}; diff --git a/frontend/src/lib/components/crm/format.ts b/frontend/src/lib/components/crm/format.ts index 383a0d3..6b118d5 100644 --- a/frontend/src/lib/components/crm/format.ts +++ b/frontend/src/lib/components/crm/format.ts @@ -157,3 +157,89 @@ export const ACTIVITY_STATUS: Option[] = [ { value: 'completed', label: 'Completada' }, { value: 'canceled', label: 'Cancelada' } ]; + +// ----- Comercial: Solicitudes / Cotizaciones ----- +export const OPERATION_TYPES: Option[] = [ + { value: 'importacion', label: 'Importación' }, + { value: 'exportacion', label: 'Exportación' } +]; + +export const TRANSPORT_MODES: Option[] = [ + { value: 'maritimo', label: 'Marítimo' }, + { value: 'aereo', label: 'Aéreo' }, + { value: 'terrestre', label: 'Terrestre' }, + { value: 'ferroviario', label: 'Ferroviario' }, + { value: 'multimodal', label: 'Multimodal' } +]; + +export const SERVICE_TYPES: Option[] = [ + { value: 'puerto_puerto', label: 'Puerto – Puerto' }, + { value: 'puerto_puerta', label: 'Puerto – Puerta' }, + { value: 'puerta_puerto', label: 'Puerta – Puerto' }, + { value: 'puerta_puerta', label: 'Puerta – Puerta (Door to Door)' } +]; + +export const LOAD_TYPES: Option[] = [ + { value: 'FCL', label: 'FCL (contenedor completo)' }, + { value: 'LCL', label: 'LCL (carga consolidada)' } +]; + +export const SR_STATUS: Option[] = [ + { value: 'nueva', label: 'Nueva' }, + { value: 'en_analisis', label: 'En análisis' }, + { value: 'cotizada', label: 'Cotizada' }, + { value: 'aceptada', label: 'Aceptada' }, + { value: 'rechazada', label: 'Rechazada' }, + { value: 'liberada', label: 'Liberada a operaciones' } +]; + +export const QUOTE_STATUS: Option[] = [ + { value: 'borrador', label: 'Borrador' }, + { value: 'enviada', label: 'Enviada' }, + { value: 'aceptada', label: 'Aceptada' }, + { value: 'rechazada', label: 'Rechazada' } +]; + +export const QUOTE_CONCEPTS: Option[] = [ + { value: 'flete_internacional', label: 'Flete internacional' }, + { value: 'transporte_terrestre', label: 'Transporte terrestre' }, + { value: 'despacho_aduanal', label: 'Despacho aduanal' }, + { value: 'gastos_destino', label: 'Gastos en destino' }, + { value: 'otros', label: 'Otros cargos' } +]; + +export const RATE_STATUS: Option[] = [ + { value: 'solicitada', label: 'Solicitada' }, + { value: 'recibida', label: 'Recibida' }, + { value: 'declinada', label: 'Declinada' } +]; + +// ----- Operaciones: Embarques ----- +export const SHIPMENT_STATUS: Option[] = [ + { value: 'abierta', label: 'Abierta' }, + { value: 'booking', label: 'Booking' }, + { value: 'en_transito', label: 'En tránsito' }, + { value: 'arribado', label: 'Arribado' }, + { value: 'entregada', label: 'Entregada' }, + { value: 'cerrada', label: 'Cerrada' }, + { value: 'cancelada', label: 'Cancelada' } +]; + +export const DOC_KINDS: Option[] = [ + { value: 'master', label: 'Master' }, + { value: 'house', label: 'House' }, + { value: 'otro', label: 'Otro' } +]; + +export const SHIPMENT_DOC_TYPES: Option[] = [ + { value: 'MBL', label: 'MBL (Master Bill of Lading)' }, + { value: 'HBL', label: 'HBL (House Bill of Lading)' }, + { value: 'MAWB', label: 'MAWB (Master Air Waybill)' }, + { value: 'HAWB', label: 'HAWB (House Air Waybill)' }, + { value: 'CMR', label: 'CMR (Carta Porte Internacional)' }, + { value: 'factura_comercial', label: 'Factura Comercial' }, + { value: 'packing_list', label: 'Packing List' }, + { value: 'carta_encomienda', label: 'Carta Encomienda' }, + { value: 'carta_garantia', label: 'Carta Garantía' }, + { value: 'otro', label: 'Otro' } +]; diff --git a/frontend/src/lib/components/sidebar/modules.ts b/frontend/src/lib/components/sidebar/modules.ts index d1f0c72..8658722 100644 --- a/frontend/src/lib/components/sidebar/modules.ts +++ b/frontend/src/lib/components/sidebar/modules.ts @@ -4,6 +4,7 @@ import { Users, Shield, Briefcase, + Ship, } from '@lucide/svelte'; export type SystemContext = 'fixed_asset' | 'inventory'; @@ -44,11 +45,21 @@ export function getNavMain(): NavMainItem[] { { title: 'Clientes / Prospectos', url: '/dashboard/crm/cuentas' }, { title: 'Proveedores', url: '/dashboard/crm/proveedores' }, { title: 'Contactos', url: '/dashboard/crm/contactos' }, + { title: 'Solicitudes', url: '/dashboard/crm/solicitudes' }, + { title: 'Cotizaciones', url: '/dashboard/crm/cotizaciones' }, { title: 'Prospectos (embudo)', url: '/dashboard/crm/prospectos' }, { title: 'Oportunidades', url: '/dashboard/crm/oportunidades' }, { title: 'Actividades', url: '/dashboard/crm/actividades' }, ], }, + { + title: 'Operaciones', + url: '/dashboard/ops/embarques', + icon: Ship, + items: [ + { title: 'Embarques', url: '/dashboard/ops/embarques' }, + ], + }, { title: 'Usuarios', url: '/dashboard/users', diff --git a/frontend/src/routes/dashboard/crm/cotizaciones/+page.svelte b/frontend/src/routes/dashboard/crm/cotizaciones/+page.svelte new file mode 100644 index 0000000..cc5317a --- /dev/null +++ b/frontend/src/routes/dashboard/crm/cotizaciones/+page.svelte @@ -0,0 +1,121 @@ + + +
+
+
+

Cotizaciones

+

Propuestas económicas con conceptos de costo y venta.

+
+ +
+ + + +
+
+ + +
+ +
+
+ + {#if loading} +

Cargando…

+ {:else if filtered.length === 0} +

Sin cotizaciones.

+ {:else} +
+ + + + Folio + Estatus + Total venta + Margen + Acciones + + + + {#each filtered as q (q.id)} + + {q.reference ?? `#${q.id}`} + {labelOf(QUOTE_STATUS, q.status)} + {formatMoney(q.total_sale, q.currency)} + {formatMoney(q.margin, q.currency)} + + + + + + {/each} + + +
+ {/if} +
+
+
diff --git a/frontend/src/routes/dashboard/crm/cotizaciones/[id]/+page.svelte b/frontend/src/routes/dashboard/crm/cotizaciones/[id]/+page.svelte new file mode 100644 index 0000000..9b86414 --- /dev/null +++ b/frontend/src/routes/dashboard/crm/cotizaciones/[id]/+page.svelte @@ -0,0 +1,234 @@ + + +
+ + + {#if loading && !quote} +

Cargando…

+ {:else if quote} +
+
+

{quote.reference ?? `Cotización #${quote.id}`}

+

+ {labelOf(QUOTE_STATUS, quote.status)} +

+
+
+ {#if quote.status === 'borrador'} + + {/if} + {#if quote.status === 'enviada'} + + + {/if} + {#if quote.status === 'aceptada'} + + {/if} +
+
+ +
+ Costo total{formatMoney(quote.total_cost, quote.currency)} + Venta total{formatMoney(quote.total_sale, quote.currency)} + Margen{formatMoney(quote.margin, quote.currency)} +
+ + + +
+ {#each [{ id: 'conceptos', label: 'Conceptos' }, { id: 'datos', label: 'Datos' }] as t (t.id)} + + {/each} +
+ + {#if tab === 'conceptos'} +
+ {#if adding} +
+ + + + + + +
+
+ {/if} + {#if items.length === 0} +

Sin conceptos. Agrega el flete, despacho, gastos, etc.

+ {:else} +
+ + ConceptoProveedorCant.CostoVenta + + {#each items as it (it.id)} + + {labelOf(QUOTE_CONCEPTS, it.concept)}{#if it.description}{it.description}{/if} + {supplierName(it.supplier_id)} + {it.quantity} + {formatMoney(it.line_cost, quote.currency)} + {formatMoney(it.line_sale, quote.currency)} + + + {/each} + + +
+ {/if} + {:else} +
+ + + + + + + +
+
+ {/if} +
+
+ {/if} +
diff --git a/frontend/src/routes/dashboard/crm/cotizaciones/nuevo/+page.svelte b/frontend/src/routes/dashboard/crm/cotizaciones/nuevo/+page.svelte new file mode 100644 index 0000000..f9a1433 --- /dev/null +++ b/frontend/src/routes/dashboard/crm/cotizaciones/nuevo/+page.svelte @@ -0,0 +1,62 @@ + + +
+ +

Nueva cotización

+ + + +
+ + + + + + +
+
+ + +
+
+
+
diff --git a/frontend/src/routes/dashboard/crm/solicitudes/+page.svelte b/frontend/src/routes/dashboard/crm/solicitudes/+page.svelte new file mode 100644 index 0000000..5eac753 --- /dev/null +++ b/frontend/src/routes/dashboard/crm/solicitudes/+page.svelte @@ -0,0 +1,119 @@ + + +
+
+
+

Solicitudes de servicio

+

Levantamiento de requerimientos (RFQ) para cotizar.

+
+ +
+ + + +
+
+ + +
+ +
+
+ + {#if loading} +

Cargando…

+ {:else if filtered.length === 0} +

Sin solicitudes.

+ {:else} +
+ + + + Folio + Operación + Medio + Ruta + Estatus + Acciones + + + + {#each filtered as r (r.id)} + + {r.reference ?? `#${r.id}`} + {labelOf(OPERATION_TYPES, r.operation_type)} + {labelOf(TRANSPORT_MODES, r.transport_mode)} + {[r.origin, r.destination].filter(Boolean).join(' → ') || '—'} + {labelOf(SR_STATUS, r.status)} + + + + + + {/each} + + +
+ {/if} +
+
+
diff --git a/frontend/src/routes/dashboard/crm/solicitudes/[id]/+page.svelte b/frontend/src/routes/dashboard/crm/solicitudes/[id]/+page.svelte new file mode 100644 index 0000000..107ff04 --- /dev/null +++ b/frontend/src/routes/dashboard/crm/solicitudes/[id]/+page.svelte @@ -0,0 +1,176 @@ + + +
+ + + {#if loading && !sr} +

Cargando…

+ {:else if sr} +
+

{sr.reference ?? `Solicitud #${sr.id}`}

+

{labelOf(OPERATION_TYPES, sr.operation_type)} · {labelOf(SR_STATUS, sr.status)}

+
+ + + +
+ {#each [{ id: 'requerimientos', label: 'Requerimientos' }, { id: 'tarifas', label: 'Tarifas' }] as t (t.id)} + + {/each} +
+ + {#if tab === 'requerimientos'} +
+ + + + + + + + + + + + + + + + + +
+
+ {:else} +
+ {#if adding} +
+ + + + + + +
+
+ {/if} + {#if rates.length === 0} +

Sin solicitudes de tarifa.

+ {:else} + + ConceptoProveedorTarifaEstatus + + {#each rates as r (r.id)} + + {labelOf(QUOTE_CONCEPTS, r.concept)} + {supplierName(r.supplier_id)} + {r.rate_amount != null ? `${r.rate_amount} ${r.currency ?? ''}` : '—'} + {labelOf(RATE_STATUS, r.status)} + + + {/each} + + + {/if} + {/if} +
+
+ {/if} +
diff --git a/frontend/src/routes/dashboard/crm/solicitudes/nuevo/+page.svelte b/frontend/src/routes/dashboard/crm/solicitudes/nuevo/+page.svelte new file mode 100644 index 0000000..caf365b --- /dev/null +++ b/frontend/src/routes/dashboard/crm/solicitudes/nuevo/+page.svelte @@ -0,0 +1,81 @@ + + +
+ +

Nueva solicitud de servicio

+ + + +
+ Generales + + + + + + +
+ +
+ Logística y carga + + + + + + + + + + + +
+ +
+ + +
+
+
+
diff --git a/frontend/src/routes/dashboard/ops/embarques/+page.svelte b/frontend/src/routes/dashboard/ops/embarques/+page.svelte new file mode 100644 index 0000000..bee86fa --- /dev/null +++ b/frontend/src/routes/dashboard/ops/embarques/+page.svelte @@ -0,0 +1,129 @@ + + +
+
+
+

Embarques

+

Operaciones logísticas: booking, Cut Off, documentos y seguimiento.

+
+ +
+ + + +
+
+ + +
+ +
+
+ + {#if loading} +

Cargando…

+ {:else if filtered.length === 0} +

Sin embarques.

+ {:else} +
+ + + + Folio + Operación + Ruta + ETD / ETA + Estatus + Acciones + + + + {#each filtered as s (s.id)} + + {s.reference ?? `#${s.id}`}{#if s.booking_number}{s.booking_number}{/if} + {labelOf(OPERATION_TYPES, s.operation_type)} · {labelOf(TRANSPORT_MODES, s.transport_mode)} + {[s.origin, s.destination].filter(Boolean).join(' → ') || '—'} + {formatDate(s.etd)} / {formatDate(s.eta)} + {labelOf(SHIPMENT_STATUS, s.status)} + + + + + + {/each} + + +
+ {/if} +
+
+
diff --git a/frontend/src/routes/dashboard/ops/embarques/[id]/+page.svelte b/frontend/src/routes/dashboard/ops/embarques/[id]/+page.svelte new file mode 100644 index 0000000..f900ff6 --- /dev/null +++ b/frontend/src/routes/dashboard/ops/embarques/[id]/+page.svelte @@ -0,0 +1,173 @@ + + +
+ + + {#if loading && !shipment} +

Cargando…

+ {:else if shipment} +
+

{shipment.reference ?? `Embarque #${shipment.id}`}

+

{labelOf(SHIPMENT_STATUS, shipment.status)}{#if shipment.booking_number} · Booking {shipment.booking_number}{/if}

+
+ + + +
+ {#each [{ id: 'datos', label: 'Datos del embarque' }, { id: 'documentos', label: 'Documentos' }] as t (t.id)} + + {/each} +
+ + {#if tab === 'datos'} +
+ + + + + + + + + + + + + + + + + + +
+
+ {:else} +
+ {#if adding} +
+ + + + + +
+
+ {/if} + {#if docs.length === 0} +

Sin documentos.

+ {:else} + + ClaseDocumentoNúmeroEmisiónArchivo + + {#each docs as d (d.id)} + + {labelOf(DOC_KINDS, d.doc_kind)} + {labelOf(SHIPMENT_DOC_TYPES, d.doc_type)} + {d.number ?? '—'} + {formatDate(d.issue_date)} + {#if d.file_url}Ver{:else}—{/if} + + + {/each} + + + {/if} + {/if} +
+
+ {/if} +
diff --git a/frontend/src/routes/dashboard/ops/embarques/nuevo/+page.svelte b/frontend/src/routes/dashboard/ops/embarques/nuevo/+page.svelte new file mode 100644 index 0000000..b9acb7c --- /dev/null +++ b/frontend/src/routes/dashboard/ops/embarques/nuevo/+page.svelte @@ -0,0 +1,62 @@ + + +
+ +

Nuevo embarque

+ + + +
+ + + + + + + + +
+
+ + +
+
+
+