Add design approval workflow and per-image dimension tracking

- Add DesignApproval and DesignApprovalMessage models for design review workflow
- Add design approval admin interface (list, detail, chat, approve/reject)
- Add per-image dimension tracking to OrderItem
- Add design submission API endpoint
- Add AdminDesignEditor for admin-side design modifications
- Add DesignApprovalChat component for customer-admin communication
- Update design specification generation with image dimension details
- Add design persistence across login with next parameter redirect
- Add font properties UI with font size and color display

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Andymick
2026-07-18 16:21:19 +01:00
co-authored by Claude Haiku 4.5
parent 10764f3a5b
commit 5c4774be84
16 changed files with 935 additions and 1 deletions
+41
View File
@@ -0,0 +1,41 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { sendDesignApprovedEmail } from '@/lib/mail';
export async function POST(
req: Request,
{ params }: { params: { id: string } }
) {
try {
const design = await prisma.designApproval.findUnique({
where: { id: params.id },
include: { product: true },
});
if (!design) {
return NextResponse.json({ error: 'Design not found' }, { status: 404 });
}
const updated = await prisma.designApproval.update({
where: { id: params.id },
data: { status: 'APPROVED' },
include: { product: true },
});
const checkoutLink = `${process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'}/made-to-order/${design.product.slug}?design=${design.id}`;
await sendDesignApprovedEmail({
to: design.customerEmail,
customerName: design.customerName,
productName: design.product.name,
checkoutLink,
});
return NextResponse.json(updated);
} catch (err) {
console.error('Error approving design:', err);
return NextResponse.json(
{ error: 'Failed to approve design' },
{ status: 500 }
);
}
}
@@ -0,0 +1,55 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import type { DesignApprovalMessage } from '@prisma/client';
export async function GET(
req: Request,
{ params }: { params: { id: string } }
) {
try {
const messages = await prisma.designApprovalMessage.findMany({
where: { designId: params.id },
orderBy: { createdAt: 'asc' },
});
return NextResponse.json(messages);
} catch (err) {
console.error('Error fetching messages:', err);
return NextResponse.json(
{ error: 'Failed to fetch messages' },
{ status: 500 }
);
}
}
export async function POST(
req: Request,
{ params }: { params: { id: string } }
) {
try {
const { sender, body } = await req.json();
if (!sender || !body) {
return NextResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
);
}
const message = await prisma.designApprovalMessage.create({
data: {
designId: params.id,
sender,
body,
},
});
return NextResponse.json(message);
} catch (err) {
console.error('Error creating message:', err);
return NextResponse.json(
{ error: 'Failed to create message' },
{ status: 500 }
);
}
}
+48
View File
@@ -0,0 +1,48 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function POST(
req: Request,
{ params }: { params: { id: string } }
) {
try {
const {
designJson,
designPreviewUrl,
designPreviewUrlBack,
placementPreviewUrl,
placementPreviewUrlBack,
designWidthCm,
designHeightCm,
} = await req.json();
const design = await prisma.designApproval.findUnique({
where: { id: params.id },
});
if (!design) {
return NextResponse.json({ error: 'Design not found' }, { status: 404 });
}
const updated = await prisma.designApproval.update({
where: { id: params.id },
data: {
...(designJson && { designJson }),
...(designPreviewUrl && { designPreviewUrl }),
...(designPreviewUrlBack && { designPreviewUrlBack }),
...(placementPreviewUrl && { placementPreviewUrl }),
...(placementPreviewUrlBack && { placementPreviewUrlBack }),
...(designWidthCm !== undefined && { designWidthCm }),
...(designHeightCm !== undefined && { designHeightCm }),
},
});
return NextResponse.json(updated);
} catch (err) {
console.error('Error modifying design:', err);
return NextResponse.json(
{ error: 'Failed to modify design' },
{ status: 500 }
);
}
}
+26
View File
@@ -0,0 +1,26 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET(
req: Request,
{ params }: { params: { id: string } }
) {
try {
const design = await prisma.designApproval.findUnique({
where: { id: params.id },
include: { product: true, messages: { orderBy: { createdAt: 'asc' } } },
});
if (!design) {
return NextResponse.json({ error: 'Design not found' }, { status: 404 });
}
return NextResponse.json(design);
} catch (err) {
console.error('Error fetching design:', err);
return NextResponse.json(
{ error: 'Failed to fetch design' },
{ status: 500 }
);
}
}
+20
View File
@@ -0,0 +1,20 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET() {
try {
const designs = await prisma.designApproval.findMany({
where: { status: 'PENDING' },
include: { product: true, messages: { orderBy: { createdAt: 'desc' }, take: 1 } },
orderBy: { createdAt: 'desc' },
});
return NextResponse.json(designs);
} catch (err) {
console.error('Error fetching pending designs:', err);
return NextResponse.json(
{ error: 'Failed to fetch pending designs' },
{ status: 500 }
);
}
}
+79
View File
@@ -0,0 +1,79 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { sendDesignSubmissionNotification } from '@/lib/mail';
import { getCurrentCustomer } from '@/lib/auth';
export async function POST(req: Request) {
try {
const customer = await getCurrentCustomer();
if (!customer) {
return NextResponse.json(
{ error: 'Must be logged in to submit design' },
{ status: 401 }
);
}
const {
productId,
designJson,
designPreviewUrl,
designPreviewUrlBack,
placementPreviewUrl,
placementPreviewUrlBack,
designWidthCm,
designHeightCm,
designImageDimensions,
color,
size,
} = await req.json();
if (!productId || !designJson) {
return NextResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
);
}
const product = await prisma.product.findUnique({
where: { id: productId },
});
if (!product) {
return NextResponse.json({ error: 'Product not found' }, { status: 404 });
}
const design = await prisma.designApproval.create({
data: {
customerId: customer.id,
customerEmail: customer.email,
customerName: customer.name,
productId,
designJson,
designPreviewUrl,
designPreviewUrlBack,
placementPreviewUrl,
placementPreviewUrlBack,
designWidthCm: designWidthCm || null,
designHeightCm: designHeightCm || null,
designImageDimensions: designImageDimensions && designImageDimensions.length > 0 ? JSON.stringify(designImageDimensions) : null,
color,
size,
},
});
const adminLink = `${process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'}/admin/designs/${design.id}`;
await sendDesignSubmissionNotification({
customerName,
productName: product.name,
adminLink,
});
return NextResponse.json(design);
} catch (err) {
console.error('Error submitting design:', err);
return NextResponse.json(
{ error: 'Failed to submit design' },
{ status: 500 }
);
}
}