CEDIS Pedidos generates professional PDF documents from orders using html2pdf.js, creating printable formats optimized for warehouse operations and fulfillment tracking.
Overview
The PDF generation system renders orders as printable documents with:
Multi-page support (one page per major category group)
Brand-consistent styling and logos
Warehouse tracking tables
Material lists organized by category
Order metadata and delivery information
PDF Export Page
The printable format is generated by the FormatoImprimible component:
import { useEffect , useRef , useState } from 'react'
import { useParams } from 'react-router-dom'
import { Printer } from 'lucide-react'
import { supabase } from '@/lib/supabase'
import type { Pedido , PedidoDetalle , Material , Sucursal } from '@/lib/types'
import { format , parseISO } from 'date-fns'
import { es } from 'date-fns/locale'
interface DetRow extends PedidoDetalle { material : Material }
export function FormatoImprimible () {
const { id } = useParams <{ id : string }>()
const printRef = useRef < HTMLDivElement >( null )
const [ pedido , setPedido ] = useState < Pedido & { sucursal : Sucursal } | null >( null )
const [ detalles , setDetalles ] = useState < DetRow []>([])
const [ loading , setLoading ] = useState ( true )
useEffect (() => {
if ( ! id ) return
Promise . all ([
supabase . from ( 'pedidos' ). select ( '*, sucursal:sucursales(*)' ). eq ( 'id' , id ). single (),
supabase . from ( 'pedido_detalle' ). select ( '*, material:materiales(*)' ). eq ( 'pedido_id' , id ),
]). then (([{ data : ped }, { data : dets }]) => {
if ( ped ) setPedido ( ped as Pedido & { sucursal : Sucursal })
if ( dets ) setDetalles (( dets as DetRow []). filter ( d =>
( d . cantidad_kilos ?? 0 ) > 0 || ( d . cantidad_solicitada ?? 0 ) > 0
))
setLoading ( false )
})
}, [ id ])
// ... rendering logic
}
Export Methods
The system supports two export methods:
Browser Print Uses native browser print dialog with print-optimized CSS
PDF Download Generates downloadable PDF file using html2pdf.js
Print Function
const handlePrint = () => window . print ()
PDF Generation Function
const handlePDF = async () => {
const { default : html2pdf } = await import ( 'html2pdf.js' )
html2pdf ()
. set ({
margin: 8 ,
filename: ` ${ pedido ?. codigo_pedido ?? 'pedido' } .pdf` ,
image: { type: 'jpeg' , quality: 0.98 },
html2canvas: { scale: 2 },
jsPDF: { unit: 'mm' , format: 'a4' , orientation: 'portrait' },
})
. from ( printRef . current )
. save ()
}
The PDF library is dynamically imported to reduce initial bundle size. The import only occurs when the user clicks the export button.
Document Structure
Page Blocks
The document is divided into multi-page blocks, each representing a category group:
const bloquesParaImprimir = [
{
blockKey: 'materiasPrimas' ,
sections: [{
key: 'materiasPrimas' ,
title: 'Materias Primas' ,
rows: materiasPrimas ,
type: 'standard' as const
}]
},
{
blockKey: 'varios_envases' ,
sections: [
{ key: 'varios' , title: 'Varios' , rows: varios , type: 'standard' as const },
{ key: 'envases' , title: 'Envases Vacíos' , rows: envases , type: 'envase' as const }
]
},
{
blockKey: 'esencias_colores' ,
sections: [
{ key: 'esencias' , title: 'Esencias' , rows: esencias , type: 'standard' as const },
... ( colores . length > 0 ? [{ key: 'colores' , title: 'Colores' , rows: colores , type: 'standard' as const }] : [])
]
}
]
Material Filtering by Category
const materiasPrimas = detalles . filter ( d => d . material . categoria === 'materia_prima' )
const varios = detalles . filter ( d => d . material . categoria === 'varios' )
const envases = detalles . filter ( d => d . material . categoria === 'envase_vacio' )
const esencias = detalles . filter ( d => d . material . categoria === 'esencia' )
const colores = detalles . filter ( d => d . material . categoria === 'color' )
Each page includes a comprehensive header with order information:
< div className = "px-6 py-4 border-b-[3px] border-[#1E3A6E] flex items-center justify-between bg-white" >
{ /* Left: Logo */ }
< div className = "w-1/3" >
< img src = "/LogoCH.png" alt = "Logo CH" className = "h-[45px] object-contain" />
</ div >
{ /* Center: Titles */ }
< div className = "w-1/3 text-center" >
< p className = "text-[14px] font-black text-[#1E3A6E] uppercase tracking-wide" >
Formato de Surtido
</ p >
< p className = "text-[12px] font-semibold text-gray-600 mt-1" >
Sucursal : < span className = "text-[#D4A01E]" >{pedido?.sucursal?. nombre } </ span >
</ p >
< p className = "text-[11px] text-[#2B5EA7] font-medium mt-0.5" >
Entrega :{ ' ' }
< span className = "font-bold" >
{ pedido ?. fecha_entrega
? format ( parseISO ( pedido . fecha_entrega ), "d 'de' MMMM 'de' yyyy" , { locale: es })
: '—' }
</ span >
</ p >
{ pedido ?. tipo_entrega && (
< p className = "text-[11px] text-gray-600 font-medium mt-0.5" >
Tipo :{ ' ' }
< span className = "font-bold text-[#1E3A6E]" > {pedido. tipo_entrega } </ span >
</ p >
)}
</ div >
{ /* Right: Toneladas & Folio */ }
< div className = "w-1/3 text-right" >
< div className = "inline-block bg-[#1E3A6E] text-white px-3 py-1 rounded-md mb-1 border border-[#1E3A6E] shadow-sm" >
< span className = "text-[10px] font-medium opacity-80 mr-1" > TONELADAS </ span >
< span className = "text-[14px] font-black" > { toneladas } </ span >
</ div >
< p className = "text-[12px] text-gray-500 font-bold mt-1" >
FOLIO :{ ' ' }
< span className = "text-[14px] text-[#D4A01E] font-black tracking-wider border-b border-[#D4A01E]" >
{ pedido ?. codigo_pedido }
</ span >
</ p >
</ div >
</ div >
Weight in Metric Tons
const toneladas = (( pedido ?. total_kilos ?? 0 ) / 1000 ). toFixed ( 3 )
The system converts kilograms to metric tons with 3 decimal precision for warehouse tracking.
Material Tables
Two table formats handle different material types:
Standard Materials Table
function PrintTable ({ rows , type } : { rows : DetRow []; type : 'standard' | 'envase' }) {
if ( type === 'standard' ) {
return (
< table className = "w-full" style = {{ borderCollapse : 'collapse' , fontSize : '10px' }} >
< thead >
< tr style = {{ borderBottom : '2px solid #E2E5EB' }} >
< th className = "text-left pb-1 font-semibold text-gray-500" style = {{ width : '35%' }} > MATERIAL </ th >
< th className = "text-right pb-1 font-semibold text-gray-500" style = {{ width : '10%' }} > CANT . KG </ th >
< th className = "text-right pb-1 font-semibold text-gray-500" style = {{ width : '10%' }} > CANT . SOL . </ th >
< th className = "text-center pb-1 font-semibold text-gray-500" style = {{ width : '25%' }} > LOTE </ th >
< th className = "text-center pb-1 font-semibold text-gray-500" style = {{ width : '20%' }} > PESO FÍSICO </ th >
</ tr >
</ thead >
< tbody >
{ rows . map ( r => (
< tr key = {r. id } style = {{ borderBottom : '1px solid #F4F6FA' , height : '28px' }} >
< td className = "py-1 text-gray-800 font-medium" > {r.material. nombre } </ td >
< td className = "py-1 text-right font-mono text-gray-700" > {r.cantidad_kilos ?? '—' } </ td >
< td className = "py-1 text-right font-mono text-gray-700" > {r.cantidad_solicitada ?? '—' } </ td >
< td className = "py-1 px-3" >
< div className = "border-b border-gray-300 w-full h-full pt-3" > </ div >
</ td >
< td className = "py-1 px-3" >
< div className = "border-b border-gray-300 w-full h-full pt-3" > </ div >
</ td >
</ tr >
))}
</ tbody >
</ table >
)
}
// ... envase table
}
Empty Container Table
return (
< table className = "w-full" style = {{ borderCollapse : 'collapse' , fontSize : '10px' }} >
< thead >
< tr style = {{ borderBottom : '2px solid #E2E5EB' }} >
< th className = "text-left pb-1 font-semibold text-gray-500" style = {{ width : '35%' }} > MATERIAL </ th >
< th className = "text-right pb-1 font-semibold text-gray-500" style = {{ width : '15%' }} > PESO UNI </ th >
< th className = "text-right pb-1 font-semibold text-gray-500" style = {{ width : '15%' }} > CANT . SOL . </ th >
< th className = "text-center pb-1 font-semibold text-gray-500" style = {{ width : '20%' }} > PRESENTACIÓN </ th >
< th className = "text-right pb-1 font-semibold text-gray-500" style = {{ width : '15%' }} > PESO TOT . </ th >
</ tr >
</ thead >
< tbody >
{ rows . map ( r => (
< tr key = {r. id } style = {{ borderBottom : '1px solid #F4F6FA' , height : '24px' }} >
< td className = "py-1 text-gray-800 font-medium" > {r.material. nombre } </ td >
< td className = "py-1 text-right font-mono text-gray-700" > {r.material.peso_aproximado ?? '—' } </ td >
< td className = "py-1 text-right font-mono text-gray-700" > {r.cantidad_solicitada ?? '—' } </ td >
< td className = "py-1 text-center text-gray-500" > {r.material.envase ?? '—' } </ td >
< td className = "py-1 text-right font-mono font-semibold text-gray-800" > {r.peso_total ?? 0 } </ td >
</ tr >
))}
</ tbody >
</ table >
)
Warehouse Tracking Tables
Each category page includes manual tracking grids for warehouse use:
Material Tracking Table
if ( blockKey === 'materiasPrimas' ) {
return (
< div className = "mt-10 mx-auto w-fit border-2 border-[#1E3A6E] bg-white" >
< div className = "bg-[#1E3A6E] text-white text-center font-bold text-[10px] py-1 uppercase tracking-widest" >
MATERIA
</ div >
< table className = "text-center text-[10px] border-collapse bg-white" >
< tbody >
< tr className = "bg-[#D4A01E] text-[#1E3A6E] font-bold" >
< td className = "border border-[#1E3A6E] px-2 py-1.5 w-20" > CERRADOS </ td >
< td className = "border border-[#1E3A6E] px-2 py-1.5 w-20" > ABIERTOS </ td >
< td className = "border border-[#1E3A6E] px-2 py-1.5 w-24" > METÁLICOS </ td >
< td className = "border border-[#1E3A6E] px-2 py-1.5 w-20" > OLLA 140 </ td >
< td className = "border border-[#1E3A6E] px-2 py-1.5 w-20" > CUÑETES </ td >
< td className = "border border-[#1E3A6E] px-2 py-1.5 w-16" > P60 </ td >
</ tr >
< tr >
< td className = "border border-[#1E3A6E] h-10" > </ td >
< td className = "border border-[#1E3A6E] h-10" > </ td >
< td className = "border border-[#1E3A6E] h-10" > </ td >
< td className = "border border-[#1E3A6E] h-10" > </ td >
< td className = "border border-[#1E3A6E] h-10" > </ td >
< td className = "border border-[#1E3A6E] h-10" > </ td >
</ tr >
{ /* Additional rows for P50, P30, P20, etc. */ }
</ tbody >
</ table >
</ div >
)
}
Tracking tables provide spaces for warehouse staff to manually record container counts during fulfillment.
Print Styling
Special CSS ensures proper printing:
< style >{ `
@media print {
@page { size: A4 portrait; margin: 10mm; }
body { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
.print \\ :hidden { display: none !important; }
.print \\ :p-0 { padding: 0 !important; }
.print \\ :w-full { width: 100% !important; }
}
` } </ style >
Page Dimensions
< div
className = "bg-white print:w-full print:shadow-none shadow-sm mb-4 print:mb-0"
style = {{
width : '210mm' , // A4 width
minHeight : '297mm' , // A4 height
fontFamily : 'Inter, sans-serif' ,
fontSize : '11px' ,
pageBreakAfter : index === bloquesParaImprimir . length - 1 ? 'auto' : 'always' ,
breakAfter : index === bloquesParaImprimir . length - 1 ? 'auto' : 'page' ,
}}
>
Use both pageBreakAfter and breakAfter for maximum browser compatibility with page breaks.
html2pdf.js Configuration
The PDF generation uses specific settings optimized for document quality:
{
margin : 8 , // 8mm margin on all sides
filename : ` ${ pedido ?. codigo_pedido ?? 'pedido' } .pdf` ,
image : { type : 'jpeg' , quality : 0.98 }, // High-quality JPEG
html2canvas : { scale : 2 }, // 2x resolution for clarity
jsPDF : {
unit : 'mm' , // Millimeter units
format : 'a4' , // A4 paper size
orientation : 'portrait' // Portrait orientation
},
}
The non-printable toolbar provides export controls:
< div className = "print:hidden flex items-center justify-between px-6 py-3 bg-white dark:bg-slate-900 border-b border-[#E2E5EB] dark:border-slate-800 sticky top-0 z-10 transition-colors" >
< h1 className = "text-sm font-bold text-[#1E3A6E] dark:text-blue-400" >
Formato Imprimible — { pedido ?. codigo_pedido }
</ h1 >
< div className = "flex gap-2" >
< button
onClick = { handlePrint }
className = "flex items-center gap-1.5 px-4 py-1.5 border border-[#E2E5EB] dark:border-slate-700 rounded-lg text-xs font-medium hover:bg-[#F4F6FA] dark:hover:bg-slate-800 dark:text-slate-200 transition-colors"
>
< Printer size = { 13 } /> Imprimir
</ button >
< button
onClick = { handlePDF }
className = "flex items-center gap-1.5 px-4 py-1.5 bg-[#1E3A6E] text-white rounded-lg text-xs font-semibold hover:bg-[#2B5EA7] transition-colors"
>
↓ Exportar PDF
</ button >
</ div >
</ div >
Dates are formatted in Spanish locale:
import { format , parseISO } from 'date-fns'
import { es } from 'date-fns/locale'
{ pedido ?. fecha_entrega
? format ( parseISO ( pedido . fecha_entrega ), "d 'de' MMMM 'de' yyyy" , { locale: es })
: '—' }
Example output: 5 de marzo de 2026
Loading State
if ( loading ) {
return (
< div className = "flex items-center justify-center h-screen" >
< span className = "w-8 h-8 border-4 border-[#2B5EA7] border-t-transparent rounded-full animate-spin" />
</ div >
)
}
Data Loading
Order and detail data is loaded in parallel:
Promise . all ([
supabase . from ( 'pedidos' ). select ( '*, sucursal:sucursales(*)' ). eq ( 'id' , id ). single (),
supabase . from ( 'pedido_detalle' ). select ( '*, material:materiales(*)' ). eq ( 'pedido_id' , id ),
]). then (([{ data : ped }, { data : dets }]) => {
if ( ped ) setPedido ( ped as Pedido & { sucursal : Sucursal })
if ( dets ) setDetalles (( dets as DetRow []). filter ( d =>
( d . cantidad_kilos ?? 0 ) > 0 || ( d . cantidad_solicitada ?? 0 ) > 0
))
setLoading ( false )
})
The system filters out detail lines with zero quantity to keep printed documents concise.
FAQ
Can I customize the PDF filename?
Yes, the filename is automatically set to the order code (e.g., MTY-20260305-421.pdf). The code is sourced from pedido?.codigo_pedido.
Why use html2pdf.js instead of server-side PDF generation?
Client-side PDF generation reduces server load and provides immediate downloads without network round-trips. It also leverages the same React components used for screen rendering, ensuring consistency.
What happens if images don't load in the PDF?
The logo image should be available at /LogoCH.png in the public directory. If it’s missing, the PDF will show a broken image placeholder. Ensure all static assets are properly deployed.
Can I print directly without viewing the format first?
The format must be rendered first to generate the printable layout. However, you can integrate a direct print action into the order list by programmatically opening the format page and triggering window.print().
Are the warehouse tracking tables editable in the PDF?
No, the tracking tables in the exported PDF are static. Warehouse staff fill these in by hand on the printed document during fulfillment operations.
Order Management Learn how to create orders for export
Weight Calculator Understand how weights appear in PDFs