Commit design approval workflow and related features

- Add design approval workflow components and API routes
- Update checkout process for design approval integration
- Add admin navigation for design management
- Update mail utilities for design notifications
- Update types for design approval system
- Update Prisma schema for design approval database

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Andymick
2026-07-18 15:42:30 +01:00
co-authored by Claude Haiku 4.5
parent 14fa3acc7a
commit 313943aeaa
8 changed files with 158 additions and 30 deletions
+4
View File
@@ -19,6 +19,8 @@ type CheckoutCartItem = {
designPreviewUrlBack: string | null;
placementPreviewUrl: string | null;
placementPreviewUrlBack: string | null;
designWidthCm: number | null;
designHeightCm: number | null;
};
type GuestInfo = {
@@ -111,6 +113,8 @@ export async function POST(req: Request) {
designPreviewUrlBack: i.designPreviewUrlBack,
placementPreviewUrl: i.placementPreviewUrl,
placementPreviewUrlBack: i.placementPreviewUrlBack,
designWidthCm: i.designWidthCm,
designHeightCm: i.designHeightCm,
})),
},
},
@@ -249,6 +249,15 @@ export async function GET(req: Request, { params }: { params: { id: string } })
<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>
+1
View File
@@ -133,6 +133,7 @@ export default function CheckoutPageClient({ isLoggedIn, customer }: CheckoutPag
}
console.log('📡 Fetching /api/checkout with', items.length, 'items');
console.log('📦 Cart items:', JSON.stringify(items, null, 2));
const res = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
+3
View File
@@ -27,6 +27,9 @@ export default function AdminNav() {
<Link href="/admin/inbox" className="tag-label hover:text-ink">
Inbox
</Link>
<Link href="/admin/designs" className="tag-label hover:text-ink">
Design Approvals
</Link>
</div>
);
}
+67
View File
@@ -173,3 +173,70 @@ export async function sendCustomerBroadcast({ to, name, subject, message, attach
return { sent: false as const, reason: err instanceof Error ? err.message : 'Unknown error sending email' };
}
}
type SendDesignSubmissionNotificationArgs = {
customerName: string | null;
productName: string;
adminLink: string;
};
export async function sendDesignSubmissionNotification({
customerName,
productName,
adminLink,
}: SendDesignSubmissionNotificationArgs) {
const to = process.env.ADMIN_NOTIFY_EMAIL ?? process.env.SMTP_USER;
if (!process.env.SMTP_HOST || !to) {
return { sent: false, reason: 'SMTP not configured — set SMTP_HOST etc. in .env' as const };
}
const transporter = getTransporter();
try {
await transporter.sendMail({
from: process.env.MAIL_FROM ?? process.env.SMTP_USER,
to,
subject: `New design submitted for approval${customerName ? ` from ${customerName}` : ''}`,
text: `A new ${productName} design has been submitted for your review and approval.\n\nReview it here:\n${adminLink}`,
html: `<p>A new ${productName} design has been submitted for your review and approval.</p><p><a href="${adminLink}">${adminLink}</a></p>`,
});
return { sent: true as const };
} catch (err) {
return { sent: false as const, reason: err instanceof Error ? err.message : 'Unknown error sending email' };
}
}
type SendDesignApprovedEmailArgs = {
to: string;
customerName: string | null;
productName: string;
checkoutLink: string;
};
export async function sendDesignApprovedEmail({
to,
customerName,
productName,
checkoutLink,
}: SendDesignApprovedEmailArgs) {
if (!process.env.SMTP_HOST) {
return { sent: false, reason: 'SMTP not configured — set SMTP_HOST etc. in .env' as const };
}
const transporter = getTransporter();
const greeting = customerName ? `Hi ${customerName},` : 'Hi,';
try {
await transporter.sendMail({
from: process.env.MAIL_FROM ?? process.env.SMTP_USER,
to,
subject: `Your ${productName} design has been approved!`,
text: `${greeting}\n\nYour ${productName} design has been approved! You can now add it to your cart and proceed to checkout.\n\n${checkoutLink}\n\n— Craft2Prints`,
html: `<p>${greeting}</p><p>Your ${productName} design has been approved! You can now add it to your cart and proceed to checkout.</p><p><a href="${checkoutLink}">${checkoutLink}</a></p><p>— Craft2Prints</p>`,
});
return { sent: true as const };
} catch (err) {
return { sent: false as const, reason: err instanceof Error ? err.message : 'Unknown error sending email' };
}
}
+2
View File
@@ -73,6 +73,8 @@ export type CartItem = {
designPreviewUrlBack: string | null;
placementPreviewUrl: string | null;
placementPreviewUrlBack: string | null;
designWidthCm: number | null;
designHeightCm: number | null;
};
export type ProofDTO = {