Capture canvas preview when admin sends design to customer
Add 'Send to Customer for Review' button to canvas that captures the current canvas state as preview images before sending, so customers see the modified design instead of the original submission. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
b97d015cd1
commit
87b269e57c
@@ -95,19 +95,10 @@ export default function EditDesignPage({ params }: { params: { id: string } }) {
|
|||||||
← Back to design approval
|
← Back to design approval
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<div className="mb-6">
|
||||||
<div>
|
|
||||||
<h1 className="font-display text-3xl mb-2">{design.product.name}</h1>
|
<h1 className="font-display text-3xl mb-2">{design.product.name}</h1>
|
||||||
<p className="text-sm text-muted">Editing design for {design.customerName || design.customerEmail}</p>
|
<p className="text-sm text-muted">Editing design for {design.customerName || design.customerEmail}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
onClick={handleSendToCustomer}
|
|
||||||
disabled={sending}
|
|
||||||
className="bg-clay px-6 py-3 text-sm font-medium text-paper hover:bg-clay-dark disabled:opacity-60"
|
|
||||||
>
|
|
||||||
{sending ? 'Sending…' : 'Send to Customer for Review'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Full Design Canvas */}
|
{/* Full Design Canvas */}
|
||||||
<DesignCanvas
|
<DesignCanvas
|
||||||
@@ -116,6 +107,7 @@ export default function EditDesignPage({ params }: { params: { id: string } }) {
|
|||||||
initialDesignJson={design.designJson}
|
initialDesignJson={design.designJson}
|
||||||
initialColor={design.color}
|
initialColor={design.color}
|
||||||
initialSize={design.size}
|
initialSize={design.size}
|
||||||
|
designId={params.id}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ export async function POST(
|
|||||||
{ params }: { params: { id: string } }
|
{ params }: { params: { id: string } }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const { designPreviewUrl, designPreviewUrlBack, placementPreviewUrl, placementPreviewUrlBack } = await req.json().catch(() => ({}));
|
||||||
|
|
||||||
const design = await prisma.designApproval.findUnique({
|
const design = await prisma.designApproval.findUnique({
|
||||||
where: { id: params.id },
|
where: { id: params.id },
|
||||||
include: { product: true },
|
include: { product: true },
|
||||||
@@ -16,10 +18,16 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Design not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Design not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update status to IN_REVIEW
|
// Update status to IN_REVIEW and save new preview if provided
|
||||||
|
const updateData: any = { status: 'IN_REVIEW' };
|
||||||
|
if (designPreviewUrl) updateData.designPreviewUrl = designPreviewUrl;
|
||||||
|
if (designPreviewUrlBack) updateData.designPreviewUrlBack = designPreviewUrlBack;
|
||||||
|
if (placementPreviewUrl) updateData.placementPreviewUrl = placementPreviewUrl;
|
||||||
|
if (placementPreviewUrlBack) updateData.placementPreviewUrlBack = placementPreviewUrlBack;
|
||||||
|
|
||||||
const updated = await prisma.designApproval.update({
|
const updated = await prisma.designApproval.update({
|
||||||
where: { id: params.id },
|
where: { id: params.id },
|
||||||
data: { status: 'IN_REVIEW' },
|
data: updateData,
|
||||||
include: { product: true, messages: { orderBy: { createdAt: 'asc' } } },
|
include: { product: true, messages: { orderBy: { createdAt: 'asc' } } },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -493,12 +493,14 @@ export default function DesignCanvas({
|
|||||||
initialDesignJson,
|
initialDesignJson,
|
||||||
initialColor,
|
initialColor,
|
||||||
initialSize,
|
initialSize,
|
||||||
|
designId,
|
||||||
}: {
|
}: {
|
||||||
product: ProductDTO;
|
product: ProductDTO;
|
||||||
isAdminEditing?: boolean;
|
isAdminEditing?: boolean;
|
||||||
initialDesignJson?: string;
|
initialDesignJson?: string;
|
||||||
initialColor?: string;
|
initialColor?: string;
|
||||||
initialSize?: string | null;
|
initialSize?: string | null;
|
||||||
|
designId?: string;
|
||||||
}) {
|
}) {
|
||||||
const [color, setColor] = useState(initialColor ?? product.colors[0]);
|
const [color, setColor] = useState(initialColor ?? product.colors[0]);
|
||||||
const [size, setSize] = useState<string | null>(initialSize ?? (product.sizes[0] ?? null));
|
const [size, setSize] = useState<string | null>(initialSize ?? (product.sizes[0] ?? null));
|
||||||
@@ -1037,6 +1039,52 @@ export default function DesignCanvas({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSendToCustomerForReview = async (id: string) => {
|
||||||
|
if (!stageBackRef.current || !stageFrontRef.current) return;
|
||||||
|
|
||||||
|
setSubmittingForApproval(true);
|
||||||
|
try {
|
||||||
|
// Capture front preview
|
||||||
|
const frontStage = stageFrontRef.current;
|
||||||
|
const frontGuides = frontStage.find('.printGuide');
|
||||||
|
frontGuides.forEach((g) => g.hide());
|
||||||
|
const frontDataUrl = frontStage.toDataURL({ pixelRatio: 2 });
|
||||||
|
frontGuides.forEach((g) => g.show());
|
||||||
|
|
||||||
|
// Capture back preview if exists
|
||||||
|
let backDataUrl: string | null = null;
|
||||||
|
if (hasBack && stageBackRef.current) {
|
||||||
|
const backStage = stageBackRef.current;
|
||||||
|
const backGuides = backStage.find('.printGuide');
|
||||||
|
backGuides.forEach((g) => g.hide());
|
||||||
|
backDataUrl = backStage.toDataURL({ pixelRatio: 2 });
|
||||||
|
backGuides.forEach((g) => g.show());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send to API with the new preview images
|
||||||
|
const res = await fetch(`/api/designs/${id}/send-to-customer`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
designPreviewUrl: frontDataUrl,
|
||||||
|
designPreviewUrlBack: backDataUrl,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
setSubmissionMessage('✓ Design sent to customer for review!');
|
||||||
|
setTimeout(() => setSubmissionMessage(null), 5000);
|
||||||
|
} else {
|
||||||
|
setSubmissionMessage('✗ Failed to send design');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error sending design:', err);
|
||||||
|
setSubmissionMessage('✗ Error sending design');
|
||||||
|
} finally {
|
||||||
|
setSubmittingForApproval(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function renderPhoto(v: 'front' | 'back') {
|
function renderPhoto(v: 'front' | 'back') {
|
||||||
const colorPhoto = v === 'front' ? product.colorPhotos[color] : product.colorPhotosBack[color];
|
const colorPhoto = v === 'front' ? product.colorPhotos[color] : product.colorPhotosBack[color];
|
||||||
if (colorPhoto) {
|
if (colorPhoto) {
|
||||||
@@ -1564,12 +1612,24 @@ export default function DesignCanvas({
|
|||||||
>
|
>
|
||||||
Add to bag — <Price cents={((elementsFront.length > 0 || elementsBack.length > 0) ? effectivePriceForPersonalised : product.effectivePrice) * quantity} />
|
Add to bag — <Price cents={((elementsFront.length > 0 || elementsBack.length > 0) ? effectivePriceForPersonalised : product.effectivePrice) * quantity} />
|
||||||
</button>
|
</button>
|
||||||
|
) : isAdminEditing ? (
|
||||||
|
<button
|
||||||
|
onClick={() => handleSendToCustomerForReview(params?.id || '')}
|
||||||
|
disabled={submittingForApproval}
|
||||||
|
className={`flex-1 px-6 py-3 text-sm font-medium transition-colors ${
|
||||||
|
submittingForApproval
|
||||||
|
? 'bg-muted text-muted/50 cursor-not-allowed'
|
||||||
|
: 'bg-clay text-paper hover:bg-clay-dark'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{submittingForApproval ? 'Sending…' : 'Send to Customer for Review'}
|
||||||
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={handleSubmitForApproval}
|
onClick={handleSubmitForApproval}
|
||||||
disabled={isAdminEditing || elementsFront.length === 0 && elementsBack.length === 0 || submittingForApproval || approvalStatus === 'PENDING'}
|
disabled={elementsFront.length === 0 && elementsBack.length === 0 || submittingForApproval || approvalStatus === 'PENDING'}
|
||||||
className={`flex-1 px-6 py-3 text-sm font-medium transition-colors ${
|
className={`flex-1 px-6 py-3 text-sm font-medium transition-colors ${
|
||||||
isAdminEditing || elementsFront.length === 0 && elementsBack.length === 0 || submittingForApproval || approvalStatus === 'PENDING'
|
elementsFront.length === 0 && elementsBack.length === 0 || submittingForApproval || approvalStatus === 'PENDING'
|
||||||
? 'bg-muted text-muted/50 cursor-not-allowed'
|
? 'bg-muted text-muted/50 cursor-not-allowed'
|
||||||
: 'bg-clay text-paper hover:bg-clay-dark'
|
: 'bg-clay text-paper hover:bg-clay-dark'
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
Reference in New Issue
Block a user