Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 8x 8x 8x 8x 8x 6x 6x 6x 6x 6x 6x 6x 5x 8x 1x 1x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 8x 12x 12x 12x 4x 4x 4x 4x 4x 4x 4x 12x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 12x 6x 6x 6x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 1x 1x | export const dynamic = "force-dynamic";
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import fs from "fs/promises";
import path from "path";
import {
withAdmin,
withErrorHandling,
successResponse,
ApiSuccessResponse,
ApiErrorResponse } from "@/lib/api";
/**
* GET /api/admin/images/audit
* Generate missing images report
*/
async function handleGet(): Promise<NextResponse<ApiSuccessResponse<unknown> | ApiErrorResponse>> {
// Get all products with their images
const products = await prisma.product.findMany({
include: { images: true },
orderBy: { id: "asc" } });
// Scan directory
const productsDir = path.join(process.cwd(), "public", "images", "products");
let actualFiles: string[] = [];
try {
actualFiles = await fs.readdir(productsDir);
actualFiles = actualFiles.filter((f) => f.endsWith(".webp"));
} catch {
actualFiles = [];
}
// Generate audit report
interface AuditItem {
productId: number;
title: string;
category?: number;
missingCount?: number;
actual?: number;
imageCount?: number;
status: string;
}
const missingImages: AuditItem[] = [];
const completeProducts: AuditItem[] = [];
const partialProducts: AuditItem[] = [];
for (const product of products) {
const imageCount = product.images.length;
if (imageCount === 0) {
missingImages.push({
productId: product.id,
title: product.title,
category: product.categoryId,
missingCount: 2, // At least 1 thumbnail + 1 preview expected
actual: 0,
status: "MISSING" });
} else if (imageCount === 1) {
partialProducts.push({
productId: product.id,
title: product.title,
imageCount,
status: "PARTIAL" });
} else {
completeProducts.push({
productId: product.id,
title: product.title,
imageCount,
status: "COMPLETE" });
}
}
const totalProducts = products.length;
const completionRate = totalProducts > 0
? Number(((completeProducts.length / totalProducts) * 100).toFixed(1))
: 0;
return successResponse({
summary: {
totalProducts,
completeProducts: completeProducts.length,
partialProducts: partialProducts.length,
missingProducts: missingImages.length,
completionRate,
filesInDirectory: actualFiles.length },
complete: completeProducts,
partial: partialProducts,
missing: missingImages });
}
export const GET = withErrorHandling(withAdmin(handleGet));
|