Files
Inventory_frontend/app/utils/printTemplates/machineReport.js
2025-09-17 23:11:13 +02:00

567 lines
23 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.
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 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 constructeurBadge = piece.constructeur?.name
? `<span class="print-badge print-badge--subtle">Constructeur: ${piece.constructeur.name}</span>`
: ''
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>`
: ''
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>
${constructeurBadge}
</div>
${piece.description ? `<p class="print-piece-description">${piece.description}</p>` : ''}
<div class="print-piece-meta">
<div class="print-field-mini">
<label>Constructeur</label>
<span>${piece.constructeur?.name || '—'}</span>
</div>
<div class="print-field-mini">
<label>Contact</label>
<span>${piece.constructeur?.contact || '—'}</span>
</div>
</div>
${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.constructeur?.name) {
badges.push(`Constructeur: ${component.constructeur.name}`)
}
const sectionClass = `print-section print-section--component print-section-depth-${Math.min(depth, 3)}`
const currentIndex = [...indexPath, idx + 1]
const indexLabel = currentIndex.join('.')
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>` : ''}
${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 || value.customField?.defaultValue || '—',
}))
}
const normalizeConstructeur = (constructeur) => {
if (!constructeur) return null
return {
name: constructeur.name || '—',
contact: [constructeur.email, constructeur.phone].filter(Boolean).join(' • ') || '—',
}
}
const normalizePiece = (piece) => ({
id: piece.id,
name: piece.name || 'Pièce sans nom',
description: piece.description || '',
reference: piece.reference || '',
customFields: normalizeCustomFields(piece.customFieldValues || []),
documents: normalizeDocuments(piece.documents || []),
constructeur: normalizeConstructeur(piece.constructeur),
indexPath: piece.indexPath || null,
})
const normalizeComponent = (component) => ({
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),
constructeur: normalizeConstructeur(component.constructeur),
})
export const buildMachinePrintContext = ({
machine,
machineName,
machineReference,
machineEmplacement,
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 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,
emplacement: machineEmplacement,
site: machine?.site?.name || '',
category: machine?.typeMachine?.category || '',
badges: machineBadges,
constructeur: normalizeConstructeur(machine?.constructeur),
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('Emplacement', context.machine.emplacement, 'Non défini')}
${renderPrintField('Site', context.machine.site, 'Non défini')}
${renderPrintField('Constructeur', context.machine.constructeur?.name, 'Non défini')}
${renderPrintField('Contact Constructeur', context.machine.constructeur?.contact, '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>`
}