Files
craft2prints/src/app/api/orders/[id]/design-spec/route.ts
T
Andymick 9dc62210b1 Improve: Design specification sheet now shows previews and info
- Always show design sections if preview images exist, even without elements
- Add design preview images to the spec sheet
- Add better error handling for designJson parsing
- Show more useful information even when elements table is empty
- Makes spec sheet more useful for reference and planning
2026-07-18 20:09:24 +01:00

310 lines
12 KiB
TypeScript
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 { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET(req: Request, { params }: { params: { id: string } }) {
try {
const { id } = params;
const order = await prisma.order.findUnique({
where: { id },
include: { items: true },
});
if (!order) {
return NextResponse.json({ error: 'Order not found' }, { status: 404 });
}
const item = order.items[0];
if (!item) {
return NextResponse.json({ error: 'No items in order' }, { status: 400 });
}
let design: { front: any[]; back: any[] } = { front: [], back: [] };
try {
design = JSON.parse(item.designJson) as { front: any[]; back: any[] };
} catch (e) {
console.error('Error parsing designJson:', e);
// Continue with empty design
}
const product = await prisma.product.findUnique({
where: { id: item.productId },
});
if (!product) {
return NextResponse.json({ error: 'Product not found' }, { status: 404 });
}
// Generate HTML/CSS for a printable spec sheet
const printAreaWidthMm = product.printAreaWidthMm || 200;
const printAreaHeightMm = product.printAreaHeightMm || 280;
// Calculate scale factor: pixels to millimeters
const DISPLAY = 560; // Same as in DesignCanvas
const printArea = JSON.parse(product.printArea) as {
xPct: number;
yPct: number;
wPct: number;
hPct: number;
};
const printAreaStartX = DISPLAY * printArea.xPct;
const printAreaStartY = DISPLAY * printArea.yPct;
const printAreaWidthPx = DISPLAY * printArea.wPct;
const printAreaHeightPx = DISPLAY * printArea.hPct;
// Scale: how many centimeters per pixel
// printAreaWidthMm / printAreaWidthPx = mm/px
// Then divide by 10 to get cm/px
const cmPerPx = (printAreaWidthMm / printAreaWidthPx) / 10;
// Convert pixel measurements to CM
const convertToCm = (px: number) => {
return (px * cmPerPx).toFixed(1);
};
// Convert element position (subtract offset first, then scale)
const convertPositionToCm = (elementPx: number, offsetPx: number) => {
const relativeToStart = Math.max(0, elementPx - offsetPx);
return (relativeToStart * cmPerPx).toFixed(1);
};
const generateDesignSection = (designElements: any[], side: 'Front' | 'Back', placement: string | null, designPreview: string | null) => {
// Always show the section if we have preview images, even without elements
if ((!designElements || designElements.length === 0) && !placement && !designPreview) return '';
return `
<div class="spec-section">
<h2>${side} Design</h2>
${designElements && designElements.length > 0 ? `
<div class="print-area-info">
<strong>Printable Area:</strong> ${printAreaWidthMm}mm × ${printAreaHeightMm}mm (${convertToCm(printAreaWidthPx)} cm × ${convertToCm(printAreaHeightPx)} cm)
</div>
` : ''}
${designPreview ? `
<div class="preview-container">
<p><em>Design preview (transparent background)</em></p>
<img src="${designPreview}" alt="${side} design preview" style="max-height: 300px; width: auto; border: 1px solid #ddd; margin: 10px 0;">
</div>
` : ''}
${placement ? `
<div class="mockup-container">
<p><em>Design on product with measurement guides</em></p>
<div style="position: relative; display: inline-block; margin: 15px 0;">
<img src="${placement}" alt="${side} design placement" class="mockup-image" style="max-height: 400px; width: auto; display: block; border: 1px solid #ddd;">
<svg viewBox="-60 -60 680 680" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;" preserveAspectRatio="xMidYMid meet">
<!-- Ruler marks every 2cm -->
${Array.from({ length: Math.ceil((DISPLAY / (10 * (printAreaWidthMm / 100)))) }).map((_, i) => {
const cm = i * 2;
const px = (cm * DISPLAY) / (printAreaWidthMm / 10);
if (px > DISPLAY) return '';
return `
<!-- Horizontal ruler tick at ${cm}cm -->
<line x1="${px}" y1="-5" x2="${px}" y2="0" stroke="#ddd" stroke-width="1"/>
<text x="${px}" y="-8" font-size="10" fill="#999" text-anchor="middle">${cm}cm</text>
<!-- Vertical ruler tick at ${cm}cm -->
<line x1="-5" y1="${px}" x2="0" y2="${px}" stroke="#ddd" stroke-width="1"/>
<text x="-8" y="${px + 3}" font-size="10" fill="#999" text-anchor="end">${cm}</text>
`;
}).join('')}
<!-- Print area border -->
<rect x="${printAreaStartX}" y="${printAreaStartY}" width="${printAreaWidthPx}" height="${printAreaHeightPx}"
fill="none" stroke="red" stroke-width="2" stroke-dasharray="5,5" opacity="0.8"/>
<!-- X-axis line (horizontal) -->
<line x1="${printAreaStartX}" y1="${printAreaStartY}" x2="${printAreaStartX + printAreaWidthPx}" y2="${printAreaStartY}"
stroke="blue" stroke-width="1.5" opacity="0.6"/>
<!-- Y-axis line (vertical) -->
<line x1="${printAreaStartX}" y1="${printAreaStartY}" x2="${printAreaStartX}" y2="${printAreaStartY + printAreaHeightPx}"
stroke="green" stroke-width="1.5" opacity="0.6"/>
<!-- Axis labels -->
<text x="${printAreaStartX + 5}" y="${printAreaStartY - 5}" font-size="12" fill="blue" font-weight="bold">X (0cm)</text>
<text x="${printAreaStartX - 25}" y="${printAreaStartY + 15}" font-size="12" fill="green" font-weight="bold">Y (0cm)</text>
<!-- Design elements with markers -->
${designElements.map((el, idx) => `
<circle cx="${el.x}" cy="${el.y}" r="5" fill="orange" opacity="0.9" stroke="white" stroke-width="1"/>
<text x="${el.x + 10}" y="${el.y - 10}" font-size="12" fill="orange" font-weight="bold" font-family="monospace">E${idx + 1}</text>
`).join('')}
</svg>
</div>
<div style="margin-top: 15px; font-size: 12px; color: #666; text-align: left; max-width: 400px;">
<p style="margin: 5px 0;"><strong style="color: red;">━━ Red dashed line:</strong> Print area boundary</p>
<p style="margin: 5px 0;"><strong style="color: blue;"> Blue line:</strong> X-axis (horizontal, 0cm at left edge)</p>
<p style="margin: 5px 0;"><strong style="color: green;"> Green line:</strong> Y-axis (vertical, 0cm at top edge)</p>
<p style="margin: 5px 0;"><strong style="color: brown;"> Brown dashed lines:</strong> Position guides from edges to element center</p>
<p style="margin: 5px 0;"><strong style="color: brown;">T: / L:</strong> Distance from top and left edges of garment in cm</p>
<p style="margin: 5px 0;"><strong style="color: orange;"> Orange circles:</strong> Design element positions (E1, E2, etc.)</p>
</div>
</div>
` : ''}
${designElements && designElements.length > 0 ? `
<table class="measurements-table">
<thead>
<tr>
<th>Element</th>
<th>Type</th>
<th>Position (CM)</th>
<th>Position (px)</th>
<th>Size (CM)</th>
<th>Size (px)</th>
<th>Details</th>
</tr>
</thead>
<tbody>
${designElements.map((el, idx) => `
<tr>
<td>E${idx + 1}</td>
<td class="element-type">${el.type === 'text' ? '📝 Text' : '🖼️ Image'}</td>
<td>X: ${convertPositionToCm(el.x, printAreaStartX)} | Y: ${convertPositionToCm(el.y, printAreaStartY)}</td>
<td>X: ${Math.round(el.x - printAreaStartX)} | Y: ${Math.round(el.y - printAreaStartY)}</td>
<td>${el.type === 'image' ? `${convertToCm(el.width)} × ${convertToCm(el.height)}` : 'N/A'}</td>
<td>${el.type === 'image' ? `${Math.round(el.width)} × ${Math.round(el.height)}` : 'N/A'}</td>
<td>
${el.type === 'text' ? `
Font: ${el.fontFamily}, ${Math.round(el.fontSize * cmPerPx * 2.834)}pt<br>
Color: ${el.fill}<br>
Text: "${el.text}"<br>
Rotation: ${Math.round(el.rotation)}°
` : `
Rotation: ${Math.round(el.rotation)}°
`}
</td>
</tr>
`).join('')}
</tbody>
</table>
` : `<p style="color: #666; font-style: italic;">No individual design elements to measure.</p>`}
</div>
`;
};
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Design Specification - Order ${order.id}</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
max-width: 1000px;
margin: 0 auto;
}
h1 { font-size: 24px; margin-bottom: 10px; }
.order-info {
background: #f5f5f5;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.order-info p {
margin: 5px 0;
font-size: 14px;
}
.spec-section {
margin-top: 30px;
page-break-inside: avoid;
}
.spec-section h2 {
font-size: 18px;
border-bottom: 2px solid #333;
padding-bottom: 10px;
margin-bottom: 15px;
}
.mockup-container {
text-align: center;
margin: 20px 0;
}
.mockup-image {
max-width: 100%;
height: auto;
display: block;
}
.measurements-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
font-size: 14px;
}
.measurements-table th,
.measurements-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.measurements-table th {
background: #f0f0f0;
font-weight: bold;
}
.element-type {
font-weight: bold;
color: #333;
}
.print-area-info {
background: #e8f4f8;
padding: 10px;
border-radius: 5px;
margin: 15px 0;
font-size: 13px;
}
@media print {
body { padding: 0; }
.spec-section { page-break-inside: avoid; }
}
</style>
</head>
<body>
<h1>Design Specification Sheet</h1>
<div class="order-info">
<p><strong>Order ID:</strong> ${order.id}</p>
<p><strong>Product:</strong> ${item.productName}</p>
<p><strong>Color:</strong> ${item.color} ${item.size ? `| Size: ${item.size}` : ''}</p>
${item.designWidthCm && item.designHeightCm ? `<p><strong>Design Dimensions (Overall):</strong> ${item.designWidthCm} cm × ${item.designHeightCm} cm</p>` : ''}
${item.designImageDimensions ? (() => {
try {
const imageDims = JSON.parse(item.designImageDimensions);
return imageDims && imageDims.length > 0 ? `<p><strong>Image Dimensions:</strong><ul style="margin: 5px 0 0 20px;">${imageDims.map((img, idx) => `<li>Image ${idx + 1}: ${img.widthCm} cm × ${img.heightCm} cm</li>`).join('')}</ul></p>` : '';
} catch (e) {
return '';
}
})() : ''}
<p><strong>Order Date:</strong> ${new Date(order.createdAt).toLocaleDateString()}</p>
</div>
${generateDesignSection(design.front, 'Front', item.placementPreviewUrl, item.designPreviewUrl)}
${generateDesignSection(design.back, 'Back', item.placementPreviewUrlBack, item.designPreviewUrlBack)}
<div class="spec-section" style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #ccc; font-size: 12px; color: #666;">
<p><strong>Notes:</strong></p>
<ul>
<li>All measurements are in centimeters (CM)</li>
<li>X-axis: 0cm at left edge of print area, increases to the right</li>
<li>Y-axis: 0cm at top edge of print area, increases downward</li>
<li>Print the design files separately for production</li>
<li>This specification sheet is for reference and planning only</li>
</ul>
</div>
</body>
</html>
`;
return new NextResponse(html, {
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Content-Disposition': `attachment; filename="design-spec-${order.id}.html"`,
},
});
} catch (err) {
console.error('Error generating design spec:', err);
return NextResponse.json({ error: 'Failed to generate specification' }, { status: 500 });
}
}