Skip to main content
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
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.
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
    },
}

Toolbar

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>

Date Formatting

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

Yes, the filename is automatically set to the order code (e.g., MTY-20260305-421.pdf). The code is sourced from pedido?.codigo_pedido.
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.
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.
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().
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

Build docs developers (and LLMs) love