Files
Inventory/app/utils/printTemplates/machineReport.js
Matthieu d860f24e69 feat: add product catalogue and product-aware UI
- introduce product catalogue pages, management view entries and shared product composables\n- wire product selection into component/piece flows and machine skeleton requirements\n- display linked product metadata and documents across machine, component and piece views\n- generalize model type tooling to handle PRODUCT category
2025-11-05 15:35:02 +01:00

751 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
uniqueConstructeurIds,
resolveConstructeurs,
formatConstructeurContact,
} from '~/shared/constructeurUtils'
const currencyFormatter = new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
currencyDisplay: 'narrowSymbol',
})
const formatCurrency = (value) => {
if (value === undefined || value === null || value === '') {
return null
}
const number = Number(value)
if (Number.isNaN(number)) {
return null
}
return currencyFormatter.format(number)
}
const formatSize = (size) => {
if (size === undefined || size === null) { return '—' }
if (size === 0) { return '0 B' }
const units = ['B', 'KB', 'MB', 'GB']
const index = Math.min(units.length - 1, Math.floor(Math.log(size) / Math.log(1024)))
const formatted = size / Math.pow(1024, index)
return `${formatted.toFixed(1)} ${units[index]}`
}
const renderPrintField = (label, value, fallback = '—') => {
const display = value !== undefined && value !== null && value !== '' ? value : fallback
return `<div class="print-field"><label>${label}</label><span>${display}</span></div>`
}
const renderPrintCustomFields = (fields = [], title, sectionClass = 'print-section') => {
if (!fields.length) { return '' }
const items = fields
.map(field => `<div class="print-field"><label>${field.label}</label><span>${field.value || '—'}</span></div>`)
.join('')
return `
<div class="${sectionClass}">
<h3>${title}</h3>
<div class="print-grid">
${items}
</div>
</div>
`
}
const renderPrintDocuments = (documents = [], title, sectionClass = 'print-section') => {
if (!documents.length) { return '' }
const rows = documents
.map(doc => `<tr><td>${doc.name}</td><td>${doc.type}</td><td>${doc.size}</td></tr>`)
.join('')
return `
<div class="${sectionClass}">
<h3>${title}</h3>
<table class="print-table">
<thead>
<tr>
<th>Nom</th>
<th>Type</th>
<th>Taille</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>
`
}
const renderPrintProductSummary = (product, title = 'Produit catalogue', sectionClass = 'print-piece-section') => {
if (!product) { return '' }
const infoEntries = [
{ label: 'Nom', value: product.name || '—' },
{ label: 'Référence', value: product.reference || '—' },
{ label: 'Catégorie', value: product.typeName || '—' },
{
label: 'Prix indicatif',
value: product.supplierPrice || '—',
},
{
label: 'Fournisseur(s)',
value: product.constructeurs?.length
? product.constructeurs.map((constructeur) => constructeur.name).filter(Boolean).join(', ') || '—'
: '—',
},
]
const infoMarkup = infoEntries
.map((field) => `<div class="print-field"><label>${field.label}</label><span>${field.value || '—'}</span></div>`)
.join('')
const customFieldsBlock = product.customFields?.length
? renderPrintCustomFields(product.customFields, 'Champs personnalisés du produit', 'print-subsection')
: ''
const documentsBlock = product.documents?.length
? renderPrintDocuments(product.documents, 'Documents du produit', 'print-subsection')
: ''
return `
<div class="${sectionClass}">
<h4>${title}</h4>
<div class="print-grid">
${infoMarkup}
</div>
${customFieldsBlock}
${documentsBlock}
</div>
`
}
const renderPrintPieces = (
pieces = [],
title = 'Pièces indépendantes',
sectionClass = 'print-section print-section--pieces'
) => {
if (!pieces.length) { return '' }
const cards = pieces
.map((piece, idx) => {
const indexLabel = piece.indexPath ? piece.indexPath.join('.') : `${idx + 1}`
const constructeurBadges = (piece.constructeurs || [])
.map((constructeur, badgeIdx) => {
const suffix = piece.constructeurs.length > 1 ? ` ${badgeIdx + 1}` : ''
return `<span class="print-badge print-badge--subtle">Fournisseur${suffix}: ${constructeur.name}</span>`
})
.join('')
const customFields = (piece.customFields || [])
.filter(field => field.value && field.value !== '—' && field.value !== '')
.map(
field => `
<li>
<span class="print-list-label">${field.label}</span>
<span class="print-list-value">${field.value}</span>
</li>
`
)
.join('')
const customFieldsBlock = customFields
? `<div class="print-piece-section"><h4>Champs personnalisés</h4><ul class="print-list">${customFields}</ul></div>`
: ''
const documentsBlock = (piece.documents || []).length
? `<div class="print-piece-section"><h4>Documents</h4><ul class="print-list">${piece.documents
.map(doc => `<li>${doc.name} <span class="print-list-hint">(${doc.type}${doc.size})</span></li>`)
.join('')}</ul></div>`
: ''
const productBlock = renderPrintProductSummary(piece.product, 'Produit catalogue')
return `
<div class="print-piece-card">
<div class="print-piece-header">
<span class="print-index print-index--piece">${indexLabel}</span>
<div class="print-piece-heading">
<div class="print-piece-title">${piece.name}</div>
<div class="print-piece-subtitle">${piece.reference || 'Référence non définie'}</div>
</div>
${constructeurBadges}
</div>
${piece.description ? `<p class="print-piece-description">${piece.description}</p>` : ''}
<div class="print-piece-meta">
<div class="print-field-mini">
<label>Fournisseur(s)</label>
<span>${piece.constructeurs?.length
? piece.constructeurs.map(constructeur => constructeur.name).join(', ')
: '—'}</span>
</div>
<div class="print-field-mini">
<label>Contact(s)</label>
<span>${piece.constructeurs?.length
? piece.constructeurs
.map(constructeur => constructeur.contact)
.filter(Boolean)
.join(' • ') || '—'
: '—'}</span>
</div>
</div>
${productBlock}
${customFieldsBlock}
${documentsBlock}
</div>
`
})
.join('')
return `
<div class="${sectionClass}">
<h3>${title}</h3>
<div class="print-piece-grid">
${cards}
</div>
</div>
`
}
const renderPrintComponents = (components = [], depth = 0, indexPath = []) => {
if (!components.length) { return '' }
return components
.map((component, idx) => {
const badges = []
if (component.constructeurs?.length) {
const label = component.constructeurs.map((constructeur, badgeIdx) => {
const suffix = component.constructeurs.length > 1 ? ` ${badgeIdx + 1}` : ''
return `Constructeur${suffix}: ${constructeur.name}`
})
badges.push(...label)
}
const sectionClass = `print-section print-section--component print-section-depth-${Math.min(depth, 3)}`
const currentIndex = [...indexPath, idx + 1]
const indexLabel = currentIndex.join('.')
const productBlock = renderPrintProductSummary(component.product, 'Produit catalogue', 'print-section print-subsection print-section--product')
return `
<div class="${sectionClass}">
<h3>
<span class="print-index print-index--component">${indexLabel}</span>
<span>Composant&nbsp;: ${component.name}</span>
</h3>
${component.description ? `<p class="print-muted">${component.description}</p>` : ''}
${badges.length ? `<div class="badge-group">${badges.map(badge => `<span class="print-badge">${badge}</span>`).join('')}</div>` : ''}
${productBlock}
${renderPrintCustomFields(
component.customFields,
'Champs personnalisés',
'print-section print-subsection print-section--custom-fields'
)}
${renderPrintPieces(
(component.pieces || []).map((piece, pieceIdx) => ({ ...piece, indexPath: [...currentIndex, pieceIdx + 1] })),
'Pièces du composant',
'print-section print-subsection print-section--pieces'
)}
${renderPrintDocuments(
component.documents,
'Documents du composant',
'print-section print-subsection print-section--documents'
)}
${renderPrintComponents(component.subComponents || [], depth + 1, currentIndex)}
</div>
`
})
.join('')
}
const normalizeDocuments = (docs = []) => {
return docs.map(doc => ({
id: doc.id,
name: doc.name || doc.filename || 'Document',
type: doc.mimeType || doc.type || '—',
size: formatSize(doc.size)
}))
}
const normalizeCustomFields = (values = []) => {
return values.map(value => ({
id: value.id,
label: value.customField?.name || 'Champ',
value: value.value || '—'
}))
}
const normalizeConstructeur = (constructeur) => {
if (!constructeur) { return null }
const contact = formatConstructeurContact(constructeur)
return {
id: constructeur.id || null,
name: constructeur.name || '—',
contact: contact || '—'
}
}
const normalizeConstructeurList = (...sources) => {
const ids = uniqueConstructeurIds(...sources)
const pools = sources
.flatMap((source) => {
if (Array.isArray(source)) {
if (source.length && typeof source[0] === 'object') {
return [source]
}
return []
}
if (source && typeof source === 'object' && 'id' in source) {
return [[source]]
}
return []
})
.filter(Boolean)
const resolved = resolveConstructeurs(ids, ...pools)
return resolved
.map(normalizeConstructeur)
.filter(Boolean)
}
const normalizeProduct = (product) => {
if (!product) { return null }
const constructeurs = normalizeConstructeurList(
product.constructeurs,
product.constructeur,
product.constructeurIds,
product.constructeurId,
)
return {
id: product.id || null,
name: product.name || 'Produit sans nom',
reference: product.reference || '',
supplierPrice: formatCurrency(product.supplierPrice),
typeName: product.typeProduct?.name || null,
constructeurs,
customFields: normalizeCustomFields(product.customFieldValues || []),
documents: normalizeDocuments(product.documents || []),
}
}
const normalizePiece = piece => {
const rawProduct = piece.product || null
const constructeurs = normalizeConstructeurList(
piece.constructeurs,
piece.constructeur,
piece.originalPiece?.constructeurs,
piece.originalPiece?.constructeur,
piece.constructeurIds,
piece.constructeurId,
rawProduct?.constructeurs,
rawProduct?.constructeur,
rawProduct?.constructeurIds,
rawProduct?.constructeurId,
)
const product = normalizeProduct(rawProduct)
return {
id: piece.id,
name: piece.name || 'Pièce sans nom',
description: piece.description || '',
reference: piece.reference || '',
customFields: normalizeCustomFields(piece.customFieldValues || []),
documents: normalizeDocuments(piece.documents || []),
constructeurs,
constructeur: constructeurs[0] || null,
product,
indexPath: piece.indexPath || null
}
}
const normalizeComponent = component => {
const rawProduct = component.product || null
const constructeurs = normalizeConstructeurList(
component.constructeurs,
component.constructeur,
component.originalComposant?.constructeurs,
component.originalComposant?.constructeur,
component.constructeurIds,
component.constructeurId,
rawProduct?.constructeurs,
rawProduct?.constructeur,
rawProduct?.constructeurIds,
rawProduct?.constructeurId,
)
const product = normalizeProduct(rawProduct)
return {
id: component.id,
name: component.name || 'Composant sans nom',
description: component.description || '',
customFields: normalizeCustomFields(component.customFieldValues || []),
documents: normalizeDocuments(component.documents || []),
pieces: (component.pieces || []).map(normalizePiece),
subComponents: (component.sousComposants || component.subComponents || []).map(normalizeComponent),
constructeurs,
constructeur: constructeurs[0] || null,
product,
}
}
export const buildMachinePrintContext = ({
machine,
machineName,
machineReference,
machinePieces = [],
components = [],
selection
}) => {
const selectionState = selection || {}
const machineSelection = selectionState.machine || {}
const componentSelection = selectionState.components || {}
const pieceSelection = selectionState.pieces || {}
const includeMachineInfo = machineSelection.info !== false
const includeMachineCustomFields = machineSelection.customFields !== false
const includeMachineDocuments = machineSelection.documents !== false
const isComponentSelected = (id) => {
if (!id) { return true }
if (Object.prototype.hasOwnProperty.call(componentSelection, id)) {
return componentSelection[id]
}
return true
}
const isPieceSelected = (id) => {
if (!id) { return true }
if (Object.prototype.hasOwnProperty.call(pieceSelection, id)) {
return pieceSelection[id]
}
return true
}
const machineBadges = []
if (machine?.typeMachine?.category) {
machineBadges.push(machine.typeMachine.category)
}
if (machine?.site?.name) {
machineBadges.push(`Site: ${machine.site.name}`)
}
if (machineReference) {
machineBadges.push(`Ref: ${machineReference}`)
}
const machineConstructeurs = normalizeConstructeurList(
machine?.constructeurs,
machine?.constructeur,
machine?.constructeurIds,
machine?.constructeurId,
)
const machineConstructeurNames = machineConstructeurs.length
? machineConstructeurs.map((constructeur) => constructeur.name).join(', ')
: ''
const machineConstructeurContacts = machineConstructeurs.length
? machineConstructeurs
.map((constructeur) => constructeur.contact)
.filter(Boolean)
.join(' • ')
: ''
const normalizedPieces = machinePieces
.map(normalizePiece)
.filter(piece => isPieceSelected(piece.id))
.map((piece, idx) => ({
...piece,
indexPath: [idx + 1]
}))
const normalizedComponents = components.map(normalizeComponent)
const filterComponentTree = (component) => {
const filteredPieces = (component.pieces || []).filter(piece => isPieceSelected(piece.id))
const filteredSubComponents = (component.subComponents || [])
.map(filterComponentTree)
.filter(Boolean)
const includeSelf = isComponentSelected(component.id)
const shouldInclude = includeSelf || filteredPieces.length > 0 || filteredSubComponents.length > 0
if (!shouldInclude) {
return null
}
return {
...component,
pieces: filteredPieces,
subComponents: filteredSubComponents
}
}
const filteredComponents = normalizedComponents
.map(filterComponentTree)
.filter(Boolean)
return {
generatedAt: new Date().toLocaleString('fr-FR'),
machine: {
id: machine?.id || null,
name: machineName,
description: machine?.description || '',
typeDescription: machine?.typeMachine?.description || '',
reference: machineReference,
site: machine?.site?.name || '',
category: machine?.typeMachine?.category || '',
badges: machineBadges,
constructeurs: machineConstructeurs,
constructeur: machineConstructeurs[0] || null,
constructeurNames: machineConstructeurNames,
constructeurContacts: machineConstructeurContacts,
includeInfo: includeMachineInfo,
customFields: includeMachineCustomFields
? normalizeCustomFields(machine?.customFieldValues || [])
: [],
documents: includeMachineDocuments
? normalizeDocuments(machine?.documents || [])
: []
},
components: filteredComponents,
pieces: normalizedPieces
}
}
export const buildMachinePrintHtml = (context, styles) => {
const title = context.machine.name ? `Impression - ${context.machine.name}` : 'Impression machine'
const badgesHtml = context.machine.badges
.map(badge => `<span class="print-badge">${badge}</span>`)
.join('')
const sections = []
sections.push(`
<div class="print-metadata">
<span>Généré le ${context.generatedAt}</span>
<span>Machine ID: ${context.machine.id || '—'}</span>
</div>
<div class="print-header">
<div class="title-block">
<div class="print-title">${context.machine.name || 'Machine sans nom'}</div>
<div class="print-subtitle">${
context.machine.description || context.machine.typeDescription || 'Aucune description disponible'
}</div>
</div>
<div class="badge-group">${badgesHtml}</div>
</div>
`)
if (context.machine.includeInfo) {
sections.push(`
<div class="print-section print-section--machine">
<h3>Informations générales</h3>
<div class="print-grid">
${renderPrintField('Nom', context.machine.name)}
${renderPrintField('Référence', context.machine.reference, 'Non définie')}
${renderPrintField('Site', context.machine.site, 'Non défini')}
${renderPrintField('Constructeur(s)', context.machine.constructeurNames, 'Non défini')}
${renderPrintField('Contact(s) Constructeur(s)', context.machine.constructeurContacts, 'Non défini')}
</div>
</div>
`)
}
const customFieldsSection = renderPrintCustomFields(
context.machine.customFields,
'Champs personnalisés de la machine',
'print-section print-section--custom-fields'
)
if (customFieldsSection) {
sections.push(customFieldsSection)
}
const documentsSection = renderPrintDocuments(
context.machine.documents,
'Documents liés à la machine',
'print-section print-section--documents'
)
if (documentsSection) {
sections.push(documentsSection)
}
const componentsSection = renderPrintComponents(context.components)
if (componentsSection) {
sections.push(componentsSection)
}
const piecesSection = renderPrintPieces(
context.pieces,
'Pièces indépendantes',
'print-section print-section--pieces'
)
if (piecesSection) {
sections.push(piecesSection)
}
sections.push(`
<div class="print-section print-muted">
Rapport généré automatiquement par Inventaire Pro.
</div>
`)
const content = sections.join('\n')
return `<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light only" />
<title>${title}</title>
${styles}
<style>
/* ============ Design tokens (sobre & pro) ============ */
:root{
--bg: #ffffff;
--ink: #0f172a; /* slate-900 */
--muted: #475569; /* slate-600 */
--line: #cbd5e1; /* slate-300 */
--line-soft: #e2e8f0; /* slate-200 */
--accent: #1f2937; /* gray-800 */
--accent-ink: #111827; /* gray-900 */
--brand: #0ea5e9; /* sky-500 (léger) */
--brand-ink: #075985; /* sky-800 */
--ok: #059669; /* emerald-600 */
--warn: #ea580c; /* orange-600 */
--comp: #be185d; /* pink-700 */
--radius-xs: 2px;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--shadow-0: none;
--shadow-1: 0 1px 0 rgba(15,23,42,.04);
--shadow-2: 0 2px 8px rgba(15,23,42,.06);
}
/* ============ Base ============ */
@page { margin: 16mm 14mm; }
html, body { height: 100%; }
body {
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Inter, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
background: #F9FAFB;
color: var(--ink);
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
.print-layout { max-width: 1120px; margin: 0 auto; padding: 28px; display: flex; flex-direction: column; gap: 20px; }
.print-metadata {
font-size: 11px; color: var(--muted); display: flex; justify-content: space-between; align-items: center; gap: 12px;
letter-spacing: .04em; text-transform: uppercase; border-bottom: 1px solid var(--line); padding-bottom: 8px;
}
.print-header {
display: flex; justify-content: space-between; align-items: flex-start; gap: 24px;
background: var(--bg); padding: 18px 20px; border: 1px solid var(--line); border-radius: var(--radius-md);
box-shadow: var(--shadow-2);
}
.print-header .title-block { flex: 1; }
.print-title { font-size: 26px; font-weight: 800; margin: 0 0 6px; color: var(--accent-ink); }
.print-subtitle { font-size: 14px; color: var(--muted); margin: 0; line-height: 1.5; }
.badge-group { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; }
.print-badge {
display: inline-flex; align-items: center; padding: 4px 8px; border-radius: var(--radius-sm);
font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
background: #e0f2fe; color: var(--brand-ink); border: 1px solid #bae6fd;
}
.print-badge--subtle { background: #f1f5f9; border-color: var(--line); color: #334155; }
.print-index {
display: inline-flex; align-items: center; justify-content: center; min-width: 24px; height: 24px;
border-radius: var(--radius-sm); font-size: 12px; font-weight: 700; margin-right: 8px;
box-shadow: inset 0 0 0 1px var(--line);
}
.print-index--component { background: #fde2f2; color: var(--comp); }
.print-index--piece { background: #ffedd5; color: var(--warn); }
/* ============ Sections ============ */
.print-section, .print-section.print-subsection, .print-piece-card { break-inside: avoid; page-break-inside: avoid; }
.print-section {
margin: 0; padding: 20px 22px; border-radius: var(--radius-md);
border: 1px solid var(--line); background: var(--bg); box-shadow: var(--shadow-2); position: relative;
}
.print-section + .print-section { margin-top: 8px; }
.print-section h3 {
font-size: 16px; font-weight: 800; margin: 0 0 14px; letter-spacing: .02em; color: var(--accent);
border-left: 3px solid var(--accent); padding-left: 10px;
}
.print-section--machine { border-left: 3px solid var(--brand); }
.print-section--custom-fields { border-left: 3px solid #7c3aed; }
.print-section--documents { border-left: 3px solid var(--ok); }
.print-section--component { border-left: 3px solid var(--comp); }
.print-section--pieces { border-left: 3px solid var(--warn); }
.print-section.print-subsection {
margin-top: 14px; margin-bottom: 10px; padding: 16px 18px; border-radius: var(--radius-sm);
box-shadow: var(--shadow-1); background: #fcfcfd;
}
/* ============ Fields (grid) ============ */
.print-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
.print-field {
display: flex; flex-direction: column; padding: 10px 12px; border-radius: var(--radius-sm);
background: #fafbfc; border: 1px solid var(--line-soft); min-height: 72px;
}
.print-field label { font-size: 10px; text-transform: uppercase; color: #64748b; letter-spacing: .08em; margin-bottom: 8px; }
.print-field span { font-size: 14px; color: var(--ink); font-weight: 600; line-height: 1.45; }
/* ============ Pieces ============ */
.print-piece-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; }
.print-piece-card {
padding: 14px 16px; border-radius: var(--radius-md); border: 1px solid #fed7aa; background: var(--bg); box-shadow: var(--shadow-1);
display: flex; flex-direction: column; gap: 10px;
}
.print-piece-header { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.print-piece-heading { flex: 1 1 160px; }
.print-piece-title { font-size: 15px; font-weight: 800; color: #7c2d12; }
.print-piece-subtitle { font-size: 11px; color: #a16207; text-transform: uppercase; letter-spacing: .06em; margin-top: 2px; }
.print-piece-description { font-size: 12px; color: #7c2d12; margin: 0; }
.print-piece-meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; }
.print-field-mini {
padding: 8px 10px; border-radius: var(--radius-sm); background: #fff7ed; border: 1px solid #fed7aa; display: flex; flex-direction: column; gap: 4px;
}
.print-field-mini label { font-size: 9px; text-transform: uppercase; color: #9a3412; letter-spacing: .06em; }
.print-field-mini span { font-size: 12px; font-weight: 700; color: #7c2d12; }
.print-piece-section { display: flex; flex-direction: column; gap: 6px; }
.print-piece-section h4 { font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .06em; color: #7c2d12; margin: 0; }
.print-list { margin: 0; padding-left: 16px; display: grid; gap: 4px; }
.print-list li { font-size: 12px; color: #475569; break-inside: avoid; }
.print-list-label { font-weight: 700; color: #7c2d12; margin-right: 4px; }
.print-list-value { color: var(--ink); }
.print-list-hint { color: #64748B; }
/* ============ Tables ============ */
.print-table { width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 13px; border: 1px solid var(--line); border-radius: var(--radius-sm); overflow: hidden; }
.print-table th, .print-table td { border: 1px solid var(--line-soft); padding: 10px 12px; text-align: left; vertical-align: top; }
.print-table thead th { background: #f1f5f9; font-weight: 800; letter-spacing: .02em; text-transform: uppercase; color: var(--accent); font-size: 12px; }
.print-table tr { break-inside: avoid; page-break-inside: avoid; }
.print-section--documents .print-table thead th { background: #ecfdf5; color: #065f46; }
.print-section--pieces .print-table thead th { background: #fffbeb; color: #92400e; }
.print-section--component .print-table thead th { background: #fdf2f8; color: #831843; }
/* ============ Utilities ============ */
.print-muted { color: var(--muted); }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }
/* ============ Print media ============ */
@media print {
body { background: white; }
.print-layout { padding: 0; max-width: none; }
.print-header, .print-section, .print-section.print-subsection, .print-piece-card, .print-table { box-shadow: var(--shadow-0); }
.print-section, .print-section.print-subsection, .print-piece-card { break-inside: avoid-page; page-break-inside: avoid; }
.badge-group { justify-content: flex-start; }
.print-metadata { border-bottom-color: var(--line); }
/* Force couleurs daccent légères pour économie dencre */
.print-badge { background: #e5f3fb !important; border-color: #d1e7f8 !important; }
.print-index--component { background: #fbe7f3 !important; }
.print-index--piece { background: #fff1e6 !important; }
}
</style>
</head>
<body>
<div class="print-layout">
${content}
</div>
</body>
</html>`
}