- 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
310 lines
12 KiB
TypeScript
310 lines
12 KiB
TypeScript
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 });
|
||
}
|
||
}
|