This commit is contained in:
Your Name committed 2026-06-07 17:17:01 +07:00
1 parent d325cd8763
commit c6bf2f70ac
45 files changed
+5195 -338

No files matched your search

+46 -2
View File
@@ -1,9 +1,21 @@
import { Elysia } from "elysia";
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const customersRoute = new Elysia({ prefix: "/customers" })
.get("/", async () => {
.get("/", async ({ query }) => {
const search = query.search as string | undefined;
const whereClause = search ? {
OR: [
{ displayName: { contains: search, mode: "insensitive" as any } },
{ email: { contains: search, mode: "insensitive" as any } },
{ phone: { contains: search, mode: "insensitive" as any } },
{ lineUserId: { contains: search, mode: "insensitive" as any } }
]
} : {};
return await prisma.customer.findMany({
where: whereClause,
orderBy: { createdAt: 'desc' },
include: {
_count: {
@@ -22,4 +34,36 @@ export const customersRoute = new Elysia({ prefix: "/customers" })
return { error: "Customer not found" };
}
return customer;
})
.patch("/:id/points", async ({ params: { id }, body, set }) => {
try {
const { pointsDelta, reason } = body as { pointsDelta: number, reason?: string };
const customer = await prisma.customer.findUnique({
where: { id: Number(id) }
});
if (!customer) {
set.status = 404;
return { error: "Customer not found" };
}
const newPoints = Math.max(0, customer.points + pointsDelta);
const updatedCustomer = await prisma.customer.update({
where: { id: Number(id) },
data: { points: newPoints }
});
return updatedCustomer;
} catch (err) {
console.error("[Customers] Adjust Points Error:", err);
set.status = 500;
return { error: "Failed to adjust points" };
}
}, {
body: t.Object({
pointsDelta: t.Number(),
reason: t.Optional(t.String())
})
});
+17
View File
@@ -176,6 +176,23 @@ export const ordersRoute = new Elysia({ prefix: "/orders" })
paymentSlipUrl: t.String()
})
})
.patch("/:id/tracking", async ({ params: { id }, body, set }) => {
try {
const { trackingNo } = body as any;
return await prisma.order.update({
where: { id: Number(id) },
data: { trackingNo },
include: { items: true }
});
} catch (err) {
set.status = 500;
return { error: "Failed to update tracking number" };
}
}, {
body: t.Object({
trackingNo: t.String()
})
})
.get("/customer/:customerId", async ({ params: { customerId } }) => {
return await prisma.order.findMany({
where: { customerId: Number(customerId) },
+381
View File
@@ -0,0 +1,381 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const pagesRoute = new Elysia({ prefix: "/pages" })
.get("/", async () => {
return await prisma.page.findMany({
include: {
sections: {
orderBy: { order: "asc" }
}
}
});
})
.get("/:slug", async ({ params: { slug }, set }) => {
const page = await prisma.page.findUnique({
where: { slug },
include: {
sections: {
where: { isActive: true },
orderBy: { order: "asc" }
}
}
});
if (!page) {
set.status = 404;
return { error: "Page not found" };
}
const populatedSections = await Promise.all(
page.sections.map(async (section) => {
const payload = section.payload as any;
if (!payload) return section;
const populatedPayload = { ...payload };
if (section.type === 'PRODUCTS') {
let products: any[] = [];
const limit = payload.limit || 3;
const productInclude = {
category: true,
scent: true,
variants: {
include: {
bomItems: { include: { rawMaterial: true } }
},
orderBy: { price: "asc" as const }
},
images: {
orderBy: { order: "asc" as const }
}
};
if (payload.type === 'featured') {
products = await prisma.product.findMany({
where: { categoryId: 1 },
include: productInclude,
take: limit
});
} else if (payload.type === 'scent' && payload.scentId) {
products = await prisma.product.findMany({
where: { scentId: Number(payload.scentId) },
include: productInclude,
take: limit
});
} else if (payload.type === 'custom' && Array.isArray(payload.productIds)) {
const ids = payload.productIds.map(Number);
products = await prisma.product.findMany({
where: { id: { in: ids } },
include: productInclude
});
products.sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id));
}
populatedPayload.products = products;
} else if (section.type === 'CATEGORIES') {
let categories: any[] = [];
if (Array.isArray(payload.categoryIds) && payload.categoryIds.length > 0) {
const ids = payload.categoryIds.map(Number);
categories = await prisma.category.findMany({
where: { id: { in: ids } },
include: {
_count: {
select: { products: true }
}
}
});
categories.sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id));
} else {
categories = await prisma.category.findMany({
include: {
_count: {
select: { products: true }
}
}
});
}
populatedPayload.categories = categories;
}
return {
...section,
payload: populatedPayload
};
})
);
return {
...page,
sections: populatedSections
};
})
.post("/", async ({ body, set }) => {
try {
const data = body as any;
const { id, slug, title, isPublished } = data;
if (id) {
return await prisma.page.update({
where: { id: Number(id) },
data: { slug, title, isPublished: isPublished !== false }
});
} else {
return await prisma.page.create({
data: { slug, title, isPublished: isPublished !== false }
});
}
} catch (err) {
console.error("[Pages] Error:", err);
set.status = 500;
return { error: "Failed to save page" };
}
}, {
body: t.Object({
id: t.Optional(t.Number()),
slug: t.String(),
title: t.String(),
isPublished: t.Optional(t.Boolean())
})
})
.delete("/:slug", async ({ params: { slug }, set }) => {
try {
await prisma.page.delete({
where: { id: Number(slug) }
});
return { success: true };
} catch (err) {
set.status = 500;
return { error: "Failed to delete page" };
}
})
.get("/:slug/sections", async ({ params: { slug }, set }) => {
try {
return await prisma.pageSection.findMany({
where: { pageId: Number(slug) },
orderBy: { order: "asc" }
});
} catch (err) {
set.status = 500;
return { error: "Failed to fetch page sections" };
}
})
.post("/:slug/sections", async ({ params: { slug }, body, set }) => {
try {
const pageId = Number(slug);
const { type, order, payload, isActive } = body as any;
const validationError = validateSectionPayload(type, payload);
if (validationError) {
set.status = 400;
return { error: validationError };
}
return await prisma.pageSection.create({
data: {
pageId,
type: type.toUpperCase(),
order,
payload,
isActive: isActive !== false
}
});
} catch (err) {
console.error("[PageSections] Error:", err);
set.status = 500;
return { error: "Failed to add section" };
}
}, {
body: t.Object({
type: t.String(),
order: t.Number(),
payload: t.Any(),
isActive: t.Optional(t.Boolean())
})
})
.put("/sections/:sectionId", async ({ params: { sectionId }, body, set }) => {
try {
const id = Number(sectionId);
const { type, order, payload, isActive } = body as any;
if (payload && type) {
const validationError = validateSectionPayload(type, payload);
if (validationError) {
set.status = 400;
return { error: validationError };
}
}
return await prisma.pageSection.update({
where: { id },
data: {
type: type ? type.toUpperCase() : undefined,
order,
payload,
isActive
}
});
} catch (err) {
console.error("[PageSections] Update Error:", err);
set.status = 500;
return { error: "Failed to update section" };
}
}, {
body: t.Object({
type: t.Optional(t.String()),
order: t.Optional(t.Number()),
payload: t.Optional(t.Any()),
isActive: t.Optional(t.Boolean())
})
})
.put("/:slug/sections/reorder", async ({ params: { slug }, body, set }) => {
try {
const pageId = Number(slug);
const { sectionOrders } = body as { sectionOrders: { id: number; order: number }[] };
await prisma.$transaction(
sectionOrders.map((item) =>
prisma.pageSection.update({
where: { id: item.id, pageId },
data: { order: item.order }
})
)
);
return { success: true };
} catch (err) {
console.error("[PageSections] Reorder Error:", err);
set.status = 500;
return { error: "Failed to reorder sections" };
}
}, {
body: t.Object({
sectionOrders: t.Array(t.Object({
id: t.Number(),
order: t.Number()
}))
})
})
.post("/newsletter", async ({ body, set }) => {
try {
const { email } = body as { email: string };
if (!email || !email.includes("@")) {
set.status = 400;
return { error: "Invalid email address" };
}
const sub = await prisma.newsletterSubscription.upsert({
where: { email },
update: {},
create: { email }
});
return { success: true, data: sub };
} catch (err) {
console.error("[Newsletter] Error:", err);
set.status = 500;
return { error: "Failed to subscribe" };
}
}, {
body: t.Object({
email: t.String()
})
})
.delete("/sections/:sectionId", async ({ params: { sectionId }, set }) => {
try {
await prisma.pageSection.delete({
where: { id: Number(sectionId) }
});
return { success: true };
} catch (err) {
set.status = 500;
return { error: "Failed to delete section" };
}
});
function validateSectionPayload(type: string, payload: any): string | null {
if (!payload || typeof payload !== 'object') {
return "Payload must be a JSON object";
}
switch (type.toUpperCase()) {
case 'HERO':
if (typeof payload.title !== 'string' || !payload.title.trim()) return "Hero payload must contain a non-empty string title";
if (typeof payload.description !== 'string') return "Hero payload must contain a string description";
if (typeof payload.imageUrl !== 'string' || !payload.imageUrl.trim()) return "Hero payload must contain a non-empty string imageUrl";
if (typeof payload.tagline !== 'string') return "Hero payload must contain a string tagline";
if (typeof payload.buttonText !== 'string') return "Hero payload must contain a string buttonText";
if (typeof payload.buttonLink !== 'string') return "Hero payload must contain a string buttonLink";
break;
case 'CATEGORIES':
if (typeof payload.title !== 'string' || !payload.title.trim()) return "Categories payload must contain a non-empty string title";
if (payload.subtitle !== undefined && typeof payload.subtitle !== 'string') return "Subtitle must be a string";
if (payload.categoryIds !== undefined && !Array.isArray(payload.categoryIds)) {
return "categoryIds must be an array of numbers";
}
break;
case 'PRODUCTS':
if (typeof payload.title !== 'string' || !payload.title.trim()) return "Products payload must contain a non-empty string title";
if (payload.subtitle !== undefined && typeof payload.subtitle !== 'string') return "Subtitle must be a string";
if (!['featured', 'scent', 'custom'].includes(payload.type)) {
return "Products type must be 'featured', 'scent', or 'custom'";
}
if (payload.type === 'scent' && typeof payload.scentId !== 'number') {
return "Scent products section requires a numeric scentId";
}
if (payload.type === 'custom' && !Array.isArray(payload.productIds)) {
return "Custom products section requires an array of productIds";
}
if (payload.limit !== undefined && typeof payload.limit !== 'number') {
return "Limit must be a number";
}
if (payload.layout !== undefined && !['grid', 'carousel'].includes(payload.layout)) {
return "layout must be 'grid' or 'carousel'";
}
if (payload.enableQuickShop !== undefined && typeof payload.enableQuickShop !== 'boolean') {
return "enableQuickShop must be a boolean";
}
break;
case 'BANNER':
if (typeof payload.imageUrl !== 'string' || !payload.imageUrl.trim()) return "Banner payload must contain a non-empty string imageUrl";
if (payload.linkUrl !== undefined && typeof payload.linkUrl !== 'string') return "linkUrl must be a string";
if (payload.title !== undefined && typeof payload.title !== 'string') return "title must be a string";
if (payload.description !== undefined && typeof payload.description !== 'string') return "description must be a string";
if (payload.buttonText !== undefined && typeof payload.buttonText !== 'string') return "buttonText must be a string";
break;
case 'TEXT':
if (typeof payload.content !== 'string' || !payload.content.trim()) return "Text payload must contain a non-empty string content";
if (payload.title !== undefined && typeof payload.title !== 'string') return "title must be a string";
if (payload.textAlign !== undefined && !['left', 'center', 'right'].includes(payload.textAlign)) {
return "textAlign must be 'left', 'center', or 'right'";
}
break;
case 'ANNOUNCEMENT_BAR':
if (!Array.isArray(payload.slides) || payload.slides.length === 0) return "Announcement Bar requires an array of slides";
if (payload.slides.some((s: any) => typeof s !== 'string')) return "All slides must be strings";
break;
case 'NEWSLETTER':
if (typeof payload.title !== 'string' || !payload.title.trim()) return "Newsletter payload must contain a non-empty string title";
if (typeof payload.description !== 'string') return "Newsletter description must be a string";
if (typeof payload.buttonText !== 'string' || !payload.buttonText.trim()) return "Newsletter buttonText must be a non-empty string";
if (typeof payload.showPrivacyCheckbox !== 'boolean') return "showPrivacyCheckbox must be a boolean";
break;
case 'EDITORIAL_TILE':
if (typeof payload.title !== 'string' || !payload.title.trim()) return "Editorial Tile payload must contain a non-empty string title";
if (typeof payload.description !== 'string') return "Editorial Tile description must be a string";
if (typeof payload.imageUrl !== 'string' || !payload.imageUrl.trim()) return "Editorial Tile imageUrl must be a non-empty string";
if (!['left', 'right'].includes(payload.imagePosition)) return "imagePosition must be 'left' or 'right'";
break;
case 'EDITORIAL_CAROUSEL':
if (typeof payload.title !== 'string' || !payload.title.trim()) return "Editorial Carousel payload must contain a non-empty string title";
if (!Array.isArray(payload.slides) || payload.slides.length === 0) return "Editorial Carousel requires an array of slides";
for (const [idx, slide] of payload.slides.entries()) {
if (typeof slide.title !== 'string' || !slide.title.trim()) return `Slide #${idx+1} must contain a non-empty title`;
if (typeof slide.description !== 'string') return `Slide #${idx+1} must contain a description string`;
if (typeof slide.imageUrl !== 'string' || !slide.imageUrl.trim()) return `Slide #${idx+1} must contain a non-empty imageUrl`;
if (typeof slide.category !== 'string') return `Slide #${idx+1} must contain a category string`;
}
break;
default:
return `Unknown section type: ${type}`;
}
return null;
}
+5
View File
@@ -2,6 +2,11 @@ import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const productsRoute = new Elysia({ prefix: "/products" })
.get("/scents", async () => {
return await prisma.scent.findMany({
orderBy: { name: "asc" }
});
})
.get("/", async ({ query }) => {
const categoryId = query.categoryId ? parseInt(query.categoryId) : undefined;
const scentId = query.scentId ? parseInt(query.scentId) : undefined;