This commit is contained in:
phaichayon
2026-06-11 12:46:57 +07:00
parent 1993d88306
commit 7a390cf0df
720 changed files with 100919 additions and 0 deletions

View File

@@ -0,0 +1,128 @@
import { and, eq, type InferSelectModel } from 'drizzle-orm';
import { NextRequest, NextResponse } from 'next/server';
import { products } from '@/db/schema';
import type { ProductMutationPayload } from '@/features/products/api/types';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
import { db } from '@/lib/db';
type Params = { params: Promise<{ id: string }> };
function toProductResponse(product: InferSelectModel<typeof products>) {
return {
id: product.id,
organization_id: product.organizationId,
photo_url: product.photoUrl,
name: product.name,
description: product.description,
created_at: product.createdAt.toISOString(),
price: product.price,
category: product.category,
updated_at: product.updatedAt.toISOString()
};
}
export async function GET(request: NextRequest, { params }: Params) {
try {
const { organization } = await requireOrganizationAccess();
const { id } = await params;
const product = await db.query.products.findFirst({
where: and(eq(products.id, Number(id)), eq(products.organizationId, organization.id))
});
if (!product) {
return NextResponse.json(
{ success: false, message: `Product with ID ${id} not found` },
{ status: 404 }
);
}
return NextResponse.json({
success: true,
time: new Date().toISOString(),
message: `Product with ID ${id} found`,
product: toProductResponse(product)
});
} catch (error) {
if (error instanceof AuthError) {
return NextResponse.json({ message: error.message }, { status: error.status });
}
return NextResponse.json({ message: 'Unable to load product' }, { status: 500 });
}
}
export async function PATCH(request: NextRequest, { params }: Params) {
try {
const { organization } = await requireOrganizationAccess();
const { id } = await params;
const body = (await request.json()) as ProductMutationPayload;
const existingProduct = await db.query.products.findFirst({
where: and(eq(products.id, Number(id)), eq(products.organizationId, organization.id))
});
if (!existingProduct) {
return NextResponse.json(
{ success: false, message: `Product with ID ${id} not found` },
{ status: 404 }
);
}
const [product] = await db
.update(products)
.set({
name: body.name,
category: body.category,
price: body.price,
description: body.description,
photoUrl: body.photoDataUrl || existingProduct.photoUrl,
updatedAt: new Date()
})
.where(and(eq(products.id, Number(id)), eq(products.organizationId, organization.id)))
.returning();
return NextResponse.json({
success: true,
time: new Date().toISOString(),
message: 'Product updated successfully',
product: toProductResponse(product)
});
} catch (error) {
if (error instanceof AuthError) {
return NextResponse.json({ message: error.message }, { status: error.status });
}
return NextResponse.json({ message: 'Unable to update product' }, { status: 500 });
}
}
export async function DELETE(request: NextRequest, { params }: Params) {
try {
const { organization } = await requireOrganizationAccess();
const { id } = await params;
const [deletedProduct] = await db
.delete(products)
.where(and(eq(products.id, Number(id)), eq(products.organizationId, organization.id)))
.returning({ id: products.id });
if (!deletedProduct) {
return NextResponse.json(
{ success: false, message: `Product with ID ${id} not found` },
{ status: 404 }
);
}
return NextResponse.json({
success: true,
message: 'Product deleted successfully'
});
} catch (error) {
if (error instanceof AuthError) {
return NextResponse.json({ message: error.message }, { status: error.status });
}
return NextResponse.json({ message: 'Unable to delete product' }, { status: 500 });
}
}

View File

@@ -0,0 +1,146 @@
import { and, asc, count, desc, eq, ilike, inArray, or } from 'drizzle-orm';
import { NextRequest, NextResponse } from 'next/server';
import { products } from '@/db/schema';
import type { ProductMutationPayload } from '@/features/products/api/types';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
import { db } from '@/lib/db';
const sortMap = {
name: products.name,
category: products.category,
price: products.price,
created_at: products.createdAt,
updated_at: products.updatedAt
} as const;
function buildProductImageUrl(name: string) {
return `https://placehold.co/120x120/336b4a/ffffff/png?text=${encodeURIComponent(name.slice(0, 12))}`;
}
export async function GET(request: NextRequest) {
try {
const { organization } = await requireOrganizationAccess();
const { searchParams } = request.nextUrl;
const page = Number(searchParams.get('page') ?? 1);
const limit = Number(searchParams.get('limit') ?? 10);
const categories = searchParams.get('categories');
const search = searchParams.get('search');
const sort = searchParams.get('sort');
const categoryValues = categories
? categories
.split(/[.,]/)
.map((value) => value.trim())
.filter(Boolean)
: [];
const filters = [
eq(products.organizationId, organization.id),
...(categoryValues.length ? [inArray(products.category, categoryValues)] : []),
...(search
? [
or(
ilike(products.name, `%${search}%`),
ilike(products.description, `%${search}%`),
ilike(products.category, `%${search}%`)
)!
]
: [])
];
const where = filters.length === 1 ? filters[0] : and(...filters);
const [totalResult] = await db.select({ value: count() }).from(products).where(where);
let orderByClause = desc(products.createdAt);
if (sort) {
try {
const sortItems = JSON.parse(sort) as Array<{ id: keyof typeof sortMap; desc: boolean }>;
const primarySort = sortItems[0];
if (primarySort?.id && sortMap[primarySort.id]) {
orderByClause = primarySort.desc
? desc(sortMap[primarySort.id])
: asc(sortMap[primarySort.id]);
}
} catch {
orderByClause = desc(products.createdAt);
}
}
const offset = (page - 1) * limit;
const rows = await db.select().from(products).where(where).orderBy(orderByClause).limit(limit).offset(offset);
return NextResponse.json({
success: true,
time: new Date().toISOString(),
message: 'Products loaded successfully',
total_products: totalResult?.value ?? 0,
offset,
limit,
products: rows.map((product) => ({
id: product.id,
organization_id: product.organizationId,
photo_url: product.photoUrl,
name: product.name,
description: product.description,
created_at: product.createdAt.toISOString(),
price: product.price,
category: product.category,
updated_at: product.updatedAt.toISOString()
}))
});
} catch (error) {
if (error instanceof AuthError) {
return NextResponse.json({ message: error.message }, { status: error.status });
}
return NextResponse.json({ message: 'Unable to load products' }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
try {
const { organization } = await requireOrganizationAccess();
const body = (await request.json()) as ProductMutationPayload;
const [product] = await db
.insert(products)
.values({
organizationId: organization.id,
name: body.name,
category: body.category,
price: body.price,
description: body.description,
photoUrl: body.photoDataUrl || buildProductImageUrl(body.name)
})
.returning();
return NextResponse.json(
{
success: true,
time: new Date().toISOString(),
message: 'Product created successfully',
product: {
id: product.id,
organization_id: product.organizationId,
photo_url: product.photoUrl,
name: product.name,
description: product.description,
created_at: product.createdAt.toISOString(),
price: product.price,
category: product.category,
updated_at: product.updatedAt.toISOString()
}
},
{ status: 201 }
);
} catch (error) {
if (error instanceof AuthError) {
return NextResponse.json({ message: error.message }, { status: error.status });
}
return NextResponse.json({ message: 'Unable to create product' }, { status: 500 });
}
}