This commit is contained in:
Your Name committed 2026-06-15 08:24:07 -04:00
1 parent a128b9c163
commit 7a352d4998
77 files changed
+5481 -21

No files matched your search

+37
View File
@@ -141,6 +141,43 @@ export declare const articlesRoute: Elysia<"/articles", {
};
};
};
} & {
articles: {
":id": {
translations: {
patch: {
body: unknown;
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
200: {
error: string;
success?: never;
translations?: never;
article?: never;
} | {
success: boolean;
translations: any[];
article: any;
error?: never;
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
};
};
};
};
};
}, {
derive: {};
resolve: {};
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"articles.d.ts","sourceRoot":"","sources":["articles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAGnC,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2GtB,CAAC"}
{"version":3,"file":"articles.d.ts","sourceRoot":"","sources":["articles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAGnC,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyJtB,CAAC"}
+158
View File
@@ -0,0 +1,158 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const articlesRoute = new Elysia({ prefix: "/articles" })
.get("/", async ({ query, set }) => {
try {
const { publishedOnly } = query;
const where = publishedOnly === 'true' ? { isPublished: true } : {};
return await prisma.article.findMany({
where,
orderBy: { createdAt: 'desc' }
});
}
catch (err) {
set.status = 500;
return { error: "Failed to load articles" };
}
})
.get("/:slug", async ({ params: { slug }, set }) => {
try {
const article = await prisma.article.findUnique({
where: { slug }
});
if (!article) {
set.status = 404;
return { error: "Article not found" };
}
return article;
}
catch (err) {
set.status = 500;
return { error: "Failed to fetch article" };
}
})
.get("/id/:id", async ({ params: { id }, set }) => {
try {
const article = await prisma.article.findUnique({
where: { id: parseInt(id, 10) }
});
if (!article) {
set.status = 404;
return { error: "Article not found" };
}
return article;
}
catch (err) {
set.status = 500;
return { error: "Failed to fetch article" };
}
})
.post("/", async ({ body, set }) => {
try {
const data = body;
return await prisma.article.create({
data: {
title: data.title,
slug: data.slug,
coverImage: data.coverImage,
content: data.content,
isPublished: data.isPublished
}
});
}
catch (err) {
console.error(err);
set.status = 500;
return { error: "Failed to create article. Ensure slug is unique." };
}
}, {
body: t.Object({
title: t.String(),
slug: t.String(),
coverImage: t.Optional(t.String()),
content: t.String(),
isPublished: t.Boolean()
})
})
.put("/:id", async ({ params: { id }, body, set }) => {
try {
const data = body;
return await prisma.article.update({
where: { id: parseInt(id, 10) },
data: {
title: data.title,
slug: data.slug,
coverImage: data.coverImage,
content: data.content,
isPublished: data.isPublished
}
});
}
catch (err) {
set.status = 500;
return { error: "Failed to update article" };
}
}, {
body: t.Object({
title: t.String(),
slug: t.String(),
coverImage: t.Optional(t.String()),
content: t.String(),
isPublished: t.Boolean()
})
})
.delete("/:id", async ({ params: { id }, set }) => {
try {
await prisma.article.delete({
where: { id: parseInt(id, 10) }
});
return { success: true };
}
catch (err) {
set.status = 500;
return { error: "Failed to delete article" };
}
})
.patch("/:id/translations", async ({ params, body, set }) => {
const id = Number(params.id);
if (isNaN(id)) {
set.status = 400;
return { error: "Invalid article ID" };
}
const payload = body;
const results = [];
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
for (const lang of LANGS) {
const title = typeof payload.title === 'string' ? (lang === 'th' ? payload.title : null) : payload.title?.[lang];
const content = typeof payload.content === 'string' ? (lang === 'th' ? payload.content : null) : payload.content?.[lang];
if (!title && !content)
continue;
const existing = await prisma.articleTranslation.findUnique({
where: { articleId_langCode: { articleId: id, langCode: lang } }
});
const updateData = {};
if (title)
updateData.title = title;
if (content)
updateData.content = content;
if (existing) {
const updated = await prisma.articleTranslation.update({
where: { id: existing.id },
data: updateData
});
results.push(updated);
}
else {
const created = await prisma.articleTranslation.create({
data: { articleId: id, langCode: lang, ...updateData }
});
results.push(created);
}
}
const updated = await prisma.article.findUnique({
where: { id },
include: { translations: true }
});
return { success: true, translations: results, article: updated };
});
//# sourceMappingURL=articles.js.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"articles.js","sourceRoot":"","sources":["articles.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"articles.js","sourceRoot":"","sources":["articles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,MAAM,MAAM,eAAe,CAAC;AAEnC,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,MAAM,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;KAC7D,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE;IACjC,IAAI,CAAC;QACH,MAAM,EAAE,aAAa,EAAE,GAAG,KAAmC,CAAC;QAC9D,MAAM,KAAK,GAAG,aAAa,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAEpE,OAAO,MAAM,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC;YACnC,KAAK;YACL,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;SAC/B,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC;IAC9C,CAAC;AACH,CAAC,CAAC;KACD,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;IACjD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC;YAC9C,KAAK,EAAE,EAAE,IAAI,EAAE;SAChB,CAAC,CAAC;QACH,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;YACjB,OAAO,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC;QACxC,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC;IAC9C,CAAC;AACH,CAAC,CAAC;KACD,GAAG,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;IAChD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC;YAC9C,KAAK,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE;SAChC,CAAC,CAAC;QACH,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;YACjB,OAAO,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC;QACxC,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC;IAC9C,CAAC;AACH,CAAC,CAAC;KACD,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE;IACjC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAW,CAAC;QACzB,OAAO,MAAM,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;YACjC,IAAI,EAAE;gBACJ,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,UAAU,EAAE,IAAI,CAAC,UAAU;gBAC3B,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,WAAW,EAAE,IAAI,CAAC,WAAW;aAC9B;SACF,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACnB,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,kDAAkD,EAAE,CAAC;IACvE,CAAC;AACH,CAAC,EAAE;IACD,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QACb,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;QACjB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;QAChB,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAClC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;QACnB,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE;KACzB,CAAC;CACH,CAAC;KACD,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE;IACnD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAW,CAAC;QACzB,OAAO,MAAM,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;YACjC,KAAK,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE;YAC/B,IAAI,EAAE;gBACJ,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,UAAU,EAAE,IAAI,CAAC,UAAU;gBAC3B,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,WAAW,EAAE,IAAI,CAAC,WAAW;aAC9B;SACF,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC;IAC/C,CAAC;AACH,CAAC,EAAE;IACD,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QACb,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;QACjB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;QAChB,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAClC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;QACnB,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE;KACzB,CAAC;CACH,CAAC;KACD,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;IAChD,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;YAC1B,KAAK,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE;SAChC,CAAC,CAAC;QACH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC3B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC;IAC/C,CAAC;AACH,CAAC,CAAC;KACD,KAAK,CAAC,mBAAmB,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE;IAC1D,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC7B,IAAI,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;QACd,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC;IACzC,CAAC;IAED,MAAM,OAAO,GAAG,IAAW,CAAC;IAC5B,MAAM,OAAO,GAAU,EAAE,CAAC;IAC1B,MAAM,KAAK,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAEnD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,OAAO,OAAO,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,CAAC;QACjH,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC;QAEzH,IAAI,CAAC,KAAK,IAAI,CAAC,OAAO;YAAE,SAAS;QAEjC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,UAAU,CAAC;YAC1D,KAAK,EAAE,EAAE,kBAAkB,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE;SACjE,CAAC,CAAC;QAEH,MAAM,UAAU,GAAQ,EAAE,CAAC;QAC3B,IAAI,KAAK;YAAE,UAAU,CAAC,KAAK,GAAG,KAAK,CAAC;QACpC,IAAI,OAAO;YAAE,UAAU,CAAC,OAAO,GAAG,OAAO,CAAC;QAE1C,IALine truncated
+95
View File
@@ -0,0 +1,95 @@
import { Elysia, t } from "elysia";
import { jwt } from "@elysiajs/jwt";
import bcrypt from "bcryptjs";
import prisma from "../lib/prisma";
export const authRoute = new Elysia({ prefix: "/auth" })
.use(jwt({
name: "jwt",
secret: process.env.JWT_SECRET || "homaroma_secret_2026_scent",
exp: "24h",
}))
.post("/login", async ({ body, jwt, set }) => {
const { username, password } = body;
// 1. Find user
const user = await prisma.user.findUnique({
where: { username },
});
if (!user) {
set.status = 401;
return { success: false, message: "Invalid credentials" };
}
// 2. Check password
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
set.status = 401;
return { success: false, message: "Invalid credentials" };
}
// 3. Generate Token
const token = await jwt.sign({
id: user.id,
username: user.username,
role: user.role,
});
return {
success: true,
token,
user: {
id: user.id,
username: user.username,
role: user.role,
},
};
}, {
body: t.Object({
username: t.String(),
password: t.String(),
}),
detail: {
summary: "Admin Login",
tags: ["auth"],
},
})
.post("/line", async ({ body, jwt, set }) => {
const { lineUserId, displayName, pictureUrl, email, phone } = body;
// Upsert Customer
const customer = await prisma.customer.upsert({
where: { lineUserId },
update: {
displayName,
pictureUrl,
...(email && { email }),
...(phone && { phone }),
},
create: {
lineUserId,
displayName,
pictureUrl,
email,
phone,
},
});
// Generate Token
const token = await jwt.sign({
id: customer.id,
lineUserId: customer.lineUserId,
role: "CUSTOMER",
});
return {
success: true,
token,
customer,
};
}, {
body: t.Object({
lineUserId: t.String(),
displayName: t.Optional(t.String()),
pictureUrl: t.Optional(t.String()),
email: t.Optional(t.String()),
phone: t.Optional(t.String()),
}),
detail: {
summary: "LINE Login for Customers",
tags: ["auth"],
},
});
//# sourceMappingURL=auth.js.map
+222
View File
@@ -0,0 +1,222 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
/** Resolve category name based on lang */
function resolveCatName(cat, lang) {
if (lang === 'all') {
const nameObj = {};
cat.translations?.forEach((tr) => { nameObj[tr.langCode] = tr.name; });
return nameObj;
}
const trans = cat.translations?.find((t) => t.langCode === lang)
|| cat.translations?.find((t) => t.langCode === 'th')
|| cat.translations?.[0];
return trans?.name ?? cat.name;
}
export const categoriesRoute = new Elysia({ prefix: "/categories" })
.get("/", async ({ query }) => {
const lang = query.lang || 'th';
const categories = await prisma.category.findMany({
orderBy: { name: "asc" },
include: {
translations: true,
_count: {
select: { products: true },
},
},
});
return categories.map((cat) => ({
...cat,
name: resolveCatName(cat, lang),
}));
})
.get("/:id/products", async ({ params, query, status }) => {
const id = parseInt(params.id);
const lang = query.lang || 'th';
if (isNaN(id))
return status(400, { message: "Invalid category ID" });
// Pagination params
const page = query.page ? parseInt(query.page) : 1;
const limit = query.limit ? parseInt(query.limit) : 12;
const skip = (page - 1) * limit;
const [category, total] = await Promise.all([
prisma.category.findUnique({
where: { id },
include: {
translations: true,
products: {
include: {
scent: true,
category: { include: { translations: true } },
translations: true,
variants: { orderBy: { price: "asc" } },
images: { orderBy: { order: "asc" } },
},
orderBy: { id: "asc" },
skip,
take: limit,
},
_count: {
select: { products: true }
}
},
}),
prisma.product.count({ where: { categoryId: id } }),
]);
if (!category)
return status(404, { message: "Category not found" });
return {
category: {
id: category.id,
name: resolveCatName(category, lang),
imageUrl: category.imageUrl,
_count: category._count,
},
data: category.products.map((p) => {
if (lang === 'all') {
const nameObj = {};
const descObj = {};
const detailsObj = {};
p.translations?.forEach((t) => {
nameObj[t.langCode] = t.name;
descObj[t.langCode] = t.description;
detailsObj[t.langCode] = t.details;
});
return {
...p,
name: nameObj,
description: descObj,
details: detailsObj,
category: { ...p.category, name: resolveCatName(p.category, lang) },
};
}
else {
const trans = p.translations?.find((t) => t.langCode === lang) || p.translations?.find((t) => t.langCode === 'th') || p.translations?.[0];
return {
...p,
name: trans?.name || p.scent?.name || '',
description: trans?.description,
details: trans?.details,
category: { ...p.category, name: resolveCatName(p.category, lang) },
};
}
}),
meta: {
total,
page,
limit,
lastPage: Math.ceil(total / limit),
},
};
})
.patch("/:id/translations", async ({ params, body, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid category ID" });
const payload = body;
const results = [];
for (const lang of LANGS) {
const name = typeof payload.name === 'string'
? (lang === 'th' ? payload.name : null)
: payload.name?.[lang];
if (!name)
continue;
const existing = await prisma.categoryTranslation.findUnique({
where: { categoryId_langCode: { categoryId: id, langCode: lang } }
});
if (existing) {
const updated = await prisma.categoryTranslation.update({
where: { id: existing.id },
data: { name },
});
results.push(updated);
}
else {
const created = await prisma.categoryTranslation.create({
data: { categoryId: id, langCode: lang, name },
});
results.push(created);
}
}
const updated = await prisma.category.findUnique({
where: { id },
include: { translations: true, _count: { select: { products: true } } },
});
return { success: true, translations: results, category: updated };
})
.post("/", async ({ body, status }) => {
try {
const category = await prisma.category.create({
data: {
name: body.name,
imageUrl: body.imageUrl,
// Auto-seed Thai translation
translations: { create: { langCode: 'th', name: body.name } },
},
});
return category;
}
catch (err) {
if (err.code === "P2002") {
return status(400, { message: "Category name already exists" });
}
return status(500, { message: "Failed to create category" });
}
}, {
body: t.Object({
name: t.String(),
imageUrl: t.Optional(t.Union([t.String(), t.Null()])),
})
})
.put("/:id", async ({ params, body, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid category ID" });
try {
const category = await prisma.category.update({
where: { id },
data: {
name: body.name,
imageUrl: body.imageUrl,
},
});
return category;
}
catch (err) {
if (err.code === "P2025") {
return status(404, { message: "Category not found" });
}
if (err.code === "P2002") {
return status(400, { message: "Category name already exists" });
}
console.error("Update category error:", err);
return status(500, { message: "Failed to update category" });
}
}, {
body: t.Object({
name: t.Optional(t.String()),
imageUrl: t.Optional(t.Union([t.String(), t.Null()])),
})
})
.delete("/:id", async ({ params, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid category ID" });
try {
const category = await prisma.category.findUnique({
where: { id },
include: { _count: { select: { products: true } } }
});
if (!category)
return status(404, { message: "Category not found" });
if (category._count.products > 0) {
return status(400, { message: "Cannot delete category with existing products" });
}
await prisma.category.delete({ where: { id } });
return { success: true };
}
catch (err) {
return status(500, { message: "Failed to delete category" });
}
});
//# sourceMappingURL=categories.js.map
+186
View File
@@ -0,0 +1,186 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const couponsRoute = new Elysia({ prefix: "/coupons" })
// Get all coupons (Admin)
.get("/", async () => {
return await prisma.coupon.findMany({
orderBy: { createdAt: 'desc' }
});
})
// Get active coupons (Storefront)
.get("/active", async () => {
const now = new Date();
return await prisma.coupon.findMany({
where: {
isActive: true,
OR: [
{ expiresAt: null },
{ expiresAt: { gt: now } }
]
},
orderBy: { createdAt: 'desc' }
});
})
// Create new coupon (Admin)
.post("/", async ({ body, set }) => {
try {
const data = body;
// Check if code already exists
const existing = await prisma.coupon.findUnique({
where: { code: data.code.toUpperCase() }
});
if (existing) {
set.status = 400;
return { error: "Coupon code already exists" };
}
const coupon = await prisma.coupon.create({
data: {
code: data.code.toUpperCase(),
type: data.type,
value: data.value,
minPurchase: data.minPurchase || 0,
maxDiscount: data.maxDiscount || null,
usageLimit: data.usageLimit || null,
expiresAt: data.expiresAt ? new Date(data.expiresAt) : null,
isActive: data.isActive !== undefined ? data.isActive : true
}
});
return coupon;
}
catch (err) {
console.error("[Coupons] Create Error:", err);
set.status = 500;
return { error: "Failed to create coupon" };
}
}, {
body: t.Object({
code: t.String(),
type: t.String(),
value: t.Number(),
minPurchase: t.Optional(t.Number()),
maxDiscount: t.Optional(t.Nullable(t.Number())),
usageLimit: t.Optional(t.Nullable(t.Number())),
expiresAt: t.Optional(t.Nullable(t.String())),
isActive: t.Optional(t.Boolean())
})
})
// Update coupon (Admin)
.patch("/:id", async ({ params: { id }, body, set }) => {
try {
const data = body;
const coupon = await prisma.coupon.update({
where: { id: Number(id) },
data: {
...(data.code && { code: data.code.toUpperCase() }),
...(data.type && { type: data.type }),
...(data.value !== undefined && { value: data.value }),
...(data.minPurchase !== undefined && { minPurchase: data.minPurchase }),
...(data.maxDiscount !== undefined && { maxDiscount: data.maxDiscount }),
...(data.usageLimit !== undefined && { usageLimit: data.usageLimit }),
...(data.expiresAt !== undefined && { expiresAt: data.expiresAt ? new Date(data.expiresAt) : null }),
...(data.isActive !== undefined && { isActive: data.isActive })
}
});
return coupon;
}
catch (err) {
console.error("[Coupons] Update Error:", err);
set.status = 500;
return { error: "Failed to update coupon" };
}
}, {
body: t.Object({
code: t.Optional(t.String()),
type: t.Optional(t.String()),
value: t.Optional(t.Number()),
minPurchase: t.Optional(t.Number()),
maxDiscount: t.Optional(t.Nullable(t.Number())),
usageLimit: t.Optional(t.Nullable(t.Number())),
expiresAt: t.Optional(t.Nullable(t.String())),
isActive: t.Optional(t.Boolean())
})
})
// Delete coupon (Admin)
.delete("/:id", async ({ params: { id }, set }) => {
try {
await prisma.coupon.delete({
where: { id: Number(id) }
});
return { success: true };
}
catch (err) {
set.status = 500;
return { error: "Failed to delete coupon" };
}
})
// Validate coupon (Storefront)
.post("/validate", async ({ body, set }) => {
try {
const { code, cartTotal } = body;
const upperCode = code.toUpperCase();
const coupon = await prisma.coupon.findUnique({
where: { code: upperCode }
});
if (!coupon) {
set.status = 404;
return { valid: false, error: "Coupon not found" };
}
if (!coupon.isActive) {
set.status = 400;
return { valid: false, error: "Coupon is no longer active" };
}
if (coupon.expiresAt && new Date() > coupon.expiresAt) {
set.status = 400;
return { valid: false, error: "Coupon has expired" };
}
if (coupon.usageLimit && coupon.usedCount >= coupon.usageLimit) {
set.status = 400;
return { valid: false, error: "Coupon usage limit reached" };
}
if (cartTotal < coupon.minPurchase) {
set.status = 400;
return { valid: false, error: `Minimum purchase of ฿${coupon.minPurchase} required` };
}
// Calculate discount amount
let discountAmount = 0;
if (coupon.type === "PERCENTAGE") {
discountAmount = cartTotal * (coupon.value / 100);
if (coupon.maxDiscount && discountAmount > coupon.maxDiscount) {
discountAmount = coupon.maxDiscount;
}
}
else if (coupon.type === "FIXED_AMOUNT") {
discountAmount = coupon.value;
}
else if (coupon.type === "FREE_SHIPPING") {
// We handle free shipping separately, returning discountAmount = 0 but a flag
return {
valid: true,
coupon,
discountAmount: 0,
isFreeShipping: true
};
}
// Ensure discount doesn't exceed cart total
if (discountAmount > cartTotal) {
discountAmount = cartTotal;
}
return {
valid: true,
coupon,
discountAmount,
isFreeShipping: false
};
}
catch (err) {
console.error("[Coupons] Validate Error:", err);
set.status = 500;
return { error: "Failed to validate coupon" };
}
}, {
body: t.Object({
code: t.String(),
cartTotal: t.Number()
})
});
//# sourceMappingURL=coupons.js.map
+63
View File
@@ -0,0 +1,63 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const customersRoute = new Elysia({ prefix: "/customers" })
.get("/", async ({ query }) => {
const search = query.search;
const whereClause = search ? {
OR: [
{ displayName: { contains: search, mode: "insensitive" } },
{ email: { contains: search, mode: "insensitive" } },
{ phone: { contains: search, mode: "insensitive" } },
{ lineUserId: { contains: search, mode: "insensitive" } }
]
} : {};
return await prisma.customer.findMany({
where: whereClause,
orderBy: { createdAt: 'desc' },
include: {
_count: {
select: { orders: true }
}
}
});
})
.get("/:id", async ({ params: { id }, set }) => {
const customer = await prisma.customer.findUnique({
where: { id: Number(id) },
include: { orders: { orderBy: { createdAt: 'desc' } } }
});
if (!customer) {
set.status = 404;
return { error: "Customer not found" };
}
return customer;
})
.patch("/:id/points", async ({ params: { id }, body, set }) => {
try {
const { pointsDelta, reason } = body;
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())
})
});
//# sourceMappingURL=customers.js.map
+38 -1
View File
@@ -49,11 +49,11 @@ export declare const heroRoute: Elysia<"/hero", {
post: {
body: {
id?: number;
imageUrl: string;
title: string;
isActive: boolean;
tagline: string;
description: string;
imageUrl: string;
buttonText: string;
buttonLink: string;
};
@@ -131,6 +131,43 @@ export declare const heroRoute: Elysia<"/hero", {
};
};
};
} & {
hero: {
":id": {
translations: {
patch: {
body: unknown;
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
200: {
error: string;
success?: never;
translations?: never;
hero?: never;
} | {
success: boolean;
translations: any[];
hero: any;
error?: never;
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
};
};
};
};
};
}, {
derive: {};
resolve: {};
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"hero.d.ts","sourceRoot":"","sources":["hero.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAGnC,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyElB,CAAC"}
{"version":3,"file":"hero.d.ts","sourceRoot":"","sources":["hero.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAGnC,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2HlB,CAAC"}
+128
View File
@@ -0,0 +1,128 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const heroRoute = new Elysia({ prefix: "/hero" })
.get("/", async () => {
return await prisma.hero.findFirst({
where: { isActive: true },
orderBy: { updatedAt: 'desc' }
}) || await prisma.hero.findFirst({
orderBy: { updatedAt: 'desc' }
});
})
.get("/all", async () => {
return await prisma.hero.findMany({
orderBy: { updatedAt: 'desc' }
});
})
.post("/", async ({ body, set }) => {
try {
const data = body;
const { id, ...heroData } = data;
if (id) {
return await prisma.hero.update({
where: { id: Number(id) },
data: heroData
});
}
else {
return await prisma.hero.create({
data: heroData
});
}
}
catch (err) {
console.error("[Hero] Error:", err);
set.status = 500;
return { error: "Failed to save hero data" };
}
}, {
body: t.Object({
id: t.Optional(t.Number()),
tagline: t.String(),
title: t.String(),
description: t.String(),
imageUrl: t.String(),
buttonText: t.String(),
buttonLink: t.String(),
isActive: t.Boolean()
})
})
.patch("/:id/activate", async ({ params: { id }, set }) => {
try {
await prisma.$transaction([
prisma.hero.updateMany({
data: { isActive: false }
}),
prisma.hero.update({
where: { id: Number(id) },
data: { isActive: true }
})
]);
return { success: true };
}
catch (err) {
set.status = 500;
return { error: "Failed to activate hero" };
}
})
.delete("/:id", async ({ params: { id }, set }) => {
try {
await prisma.hero.delete({
where: { id: Number(id) }
});
return { success: true };
}
catch (err) {
set.status = 500;
return { error: "Failed to delete hero" };
}
})
.patch("/:id/translations", async ({ params, body, set }) => {
const id = Number(params.id);
if (isNaN(id)) {
set.status = 400;
return { error: "Invalid hero ID" };
}
const payload = body;
const results = [];
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
for (const lang of LANGS) {
const tagline = typeof payload.tagline === 'string' ? (lang === 'th' ? payload.tagline : null) : payload.tagline?.[lang];
const title = typeof payload.title === 'string' ? (lang === 'th' ? payload.title : null) : payload.title?.[lang];
const description = typeof payload.description === 'string' ? (lang === 'th' ? payload.description : null) : payload.description?.[lang];
const buttonText = typeof payload.buttonText === 'string' ? (lang === 'th' ? payload.buttonText : null) : payload.buttonText?.[lang];
if (!tagline && !title && !description && !buttonText)
continue;
const existing = await prisma.heroTranslation.findUnique({
where: { heroId_langCode: { heroId: id, langCode: lang } }
});
const updateData = {};
if (tagline)
updateData.tagline = tagline;
if (title)
updateData.title = title;
if (description)
updateData.description = description;
if (buttonText)
updateData.buttonText = buttonText;
if (existing) {
const updated = await prisma.heroTranslation.update({
where: { id: existing.id },
data: updateData
});
results.push(updated);
}
else {
const created = await prisma.heroTranslation.create({
data: { heroId: id, langCode: lang, ...updateData }
});
results.push(created);
}
}
const updated = await prisma.hero.findUnique({
where: { id },
include: { translations: true }
});
return { success: true, translations: results, hero: updated };
});
//# sourceMappingURL=hero.js.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"hero.js","sourceRoot":"","sources":["hero.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"hero.js","sourceRoot":"","sources":["hero.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,MAAM,MAAM,eAAe,CAAC;AAEnC,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,MAAM,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;KACrD,GAAG,CAAC,GAAG,EAAE,KAAK,IAAI,EAAE;IACnB,OAAO,MAAM,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC;QACjC,KAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;QACzB,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;KAC/B,CAAC,IAAI,MAAM,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC;QAChC,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;KAC/B,CAAC,CAAC;AACL,CAAC,CAAC;KACD,GAAG,CAAC,MAAM,EAAE,KAAK,IAAI,EAAE;IACtB,OAAO,MAAM,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC;QAChC,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;KAC/B,CAAC,CAAC;AACL,CAAC,CAAC;KACD,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE;IACjC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAW,CAAC;QACzB,MAAM,EAAE,EAAE,EAAE,GAAG,QAAQ,EAAE,GAAG,IAAI,CAAC;QAEjC,IAAI,EAAE,EAAE,CAAC;YACP,OAAO,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;gBAC9B,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EAAE;gBACzB,IAAI,EAAE,QAAQ;aACf,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,OAAO,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;gBAC9B,IAAI,EAAE,QAAQ;aACf,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC;QACpC,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC;IAC/C,CAAC;AACH,CAAC,EAAE;IACD,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QACb,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAC1B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;QACnB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;QACjB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;QACvB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;QACpB,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;QACtB,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;QACtB,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE;KACtB,CAAC;CACH,CAAC;KACD,KAAK,CAAC,eAAe,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;IACxD,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,YAAY,CAAC;YACxB,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC;gBACrB,IAAI,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE;aAC1B,CAAC;YACF,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;gBACjB,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EAAE;gBACzB,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;aACzB,CAAC;SACH,CAAC,CAAC;QACH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC3B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC;IAC9C,CAAC;AACH,CAAC,CAAC;KACD,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;IAChD,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;YACvB,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EAAE;SAC1B,CAAC,CAAC;QACH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC3B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC;IAC5C,CAAC;AACH,CAAC,CAAC;KACD,KAAK,CAAC,mBAAmB,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE;IAC1D,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC7B,IAAI,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;QACd,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC;IACtC,CAAC;IAED,MAAM,OAAO,GAAG,IAAW,CAAC;IAC5B,MAAM,OAAO,GAAU,EAAE,CAAC;IAC1B,MAAM,KAAK,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAEnD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC;QACzH,MAAM,KAAK,GAAG,OAAO,OAAO,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,CAAC;QACjH,MAAM,WAAW,GAAG,OAAO,OAAO,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,CAAC;QACzI,MAAM,UAAU,GAAG,OAAO,OAAO,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC,CAAC;QAErI,IAAI,CAAC,OAAO,IAAI,CAAC,KAAK,IAAI,CAAC,WAAW,IAAI,CAAC,UAAU;YAAE,SAAS;QAEhE,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,UAAU,CAAC;YACvD,KAAK,EAAE,EAAE,eAAe,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE;SAC3D,CAAC,CAAC;QAEH,MAAM,UAAU,GAAQ,EAAE,CAAC;QAC3B,IAAI,OAAO;YAAE,UAAU,CAAC,OAAO,GAAG,OAAO,CAAC;QAC1C,IAAI,KAAK;YAAE,UAAU,CAAC,KAAK,GAAG,KAAK,CAAC;QACpC,IAAI,WAAW;YAAE,UAAU,CAAC,WAAW,GAAG,WAAW,CAAC;QACtD,IAAI,UAAU;YAAE,UAAU,CAAC,UAAU,GAAG,UAAU,CAAC;QAEnD,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC;gBAClD,KAAK,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE;gBAC1B,IAAI,EAAE,UAAU;aACjB,CAAC,CAAC;YACH,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACxB,CAAC;aAAM,CAAC;YACN,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC;gBAClD,IAAI,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,UAAU,EAAE;aACpD,CAAC,CAAC;YACH,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC;QAC3C,KAAK,EAAE,EAAE,EAAE,EAAE;QACb,OAAO,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE;KAChC,CAAC,CAAC;IAEH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,Line truncated
+348
View File
@@ -0,0 +1,348 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
const toNum = (val) => {
if (val === null || val === undefined || val === "")
return 0;
const s = String(val).replace(/[^0-9.\-]/g, "");
const n = parseFloat(s);
return isNaN(n) ? 0 : n;
};
const str = (val) => String(val ?? "").trim();
const MONTHS = { Jan: '01', Feb: '02', Mar: '03', Apr: '04', May: '05', Jun: '06', Jul: '07', Aug: '08', Sep: '09', Oct: '10', Nov: '11', Dec: '12' };
const parseLazadaDate = (val) => {
const d = str(val);
if (!d)
return "";
// e.g. "25 Apr 2026 23:24"
const parts = d.split(" ");
if (parts.length >= 4) {
const [day, monthStr, year, time] = parts;
const month = MONTHS[monthStr] || "01";
return `${year}-${month}-${day.padStart(2, "0")} ${time}:00`;
}
return d;
};
export const lazadaOrderRoute = new Elysia({ prefix: "/lazada-order" })
// ── Import (Excel rows as JSON) ───────────────────────────────────────────
.post("/import", async ({ body, set }) => {
try {
const { rows, shop } = body;
const shopName = shop || "hom-aroma";
console.log(`[LazadaOrder] Received ${rows.length} raw rows for shop: ${shopName}`);
// Group rows by orderNumber
const orderMap = new Map();
for (const row of rows) {
const orderNumber = str(row["orderNumber"]);
if (!orderNumber)
continue;
if (!orderMap.has(orderNumber))
orderMap.set(orderNumber, { meta: row, items: [] });
orderMap.get(orderNumber).items.push(row);
}
console.log(`[LazadaOrder] Grouped into ${orderMap.size} unique orders`);
const CHUNK = 30;
const entries = Array.from(orderMap.entries());
let upserted = 0;
for (let i = 0; i < entries.length; i += CHUNK) {
const chunk = entries.slice(i, i + CHUNK);
await prisma.$transaction(async (tx) => {
for (const [orderNumber, { meta, items }] of chunk) {
// Sync strategy: delete old items
await tx.lazadaOrderItem.deleteMany({ where: { orderNumber } });
let totalPaidPrice = 0;
let totalShippingFee = 0;
const itemsData = items.map((item) => {
const paidPrice = toNum(item["paidPrice"]);
const shippingFee = toNum(item["shippingFee"]);
totalPaidPrice += paidPrice;
totalShippingFee += shippingFee;
return {
orderItemId: str(item["orderItemId"]),
sellerSku: str(item["sellerSku"]),
lazadaSku: str(item["lazadaSku"]),
itemName: str(item["itemName"]),
variation: str(item["variation"]),
unitPrice: toNum(item["unitPrice"]),
paidPrice,
sellerDiscountTotal: toNum(item["sellerDiscountTotal"]),
platformDiscountTotal: toNum(item["platformDiscountTotal"]),
shippingFee,
status: str(item["status"]),
};
});
// Decide order status based on items (or first item)
// We'll just use the first item's status if they share it
const orderStatus = str(meta["status"]);
const orderData = {
shop: shopName,
createTime: parseLazadaDate(meta["createTime"]),
updateTime: parseLazadaDate(meta["updateTime"]),
deliveredDate: parseLazadaDate(meta["deliveredDate"]),
customerName: str(meta["customerName"]),
shippingCity: str(meta["shippingCity"]),
shippingPostCode: str(meta["shippingPostCode"]),
shippingRegion: str(meta["shippingRegion"]),
payMethod: str(meta["payMethod"]),
status: orderStatus,
trackingCode: str(meta["trackingCode"]),
shippingProvider: str(meta["shippingProvider"]),
orderAmount: totalPaidPrice,
shippingFee: totalShippingFee,
};
await tx.lazadaOrder.upsert({
where: { orderNumber },
update: { ...orderData, items: { create: itemsData } },
create: { orderNumber, ...orderData, items: { create: itemsData } },
});
upserted++;
}
}, { timeout: 30000 });
console.log(`[LazadaOrder] Progress: ${Math.min(i + CHUNK, entries.length)}/${entries.length}`);
}
console.log(`[LazadaOrder] ✅ Done — ${upserted} orders upserted`);
return { success: true, message: `นำเข้าสำเร็จ ${upserted} คำสั่งซื้อ (${rows.length} แถว)`, ordersCount: upserted, rowsCount: rows.length };
}
catch (error) {
console.error(`[LazadaOrder] ❌ Error:`, error.message);
set.status = 500;
return { success: false, message: error.message };
}
}, { body: t.Object({ rows: t.Array(t.Any()), shop: t.Optional(t.String()) }) })
// ── Clear Shop Data ───────────────────────────────────────────────────────
.delete("/clear", async ({ query, set }) => {
try {
const shop = query.shop || "hom-aroma";
const result = await prisma.lazadaOrder.deleteMany({ where: { shop } });
return { success: true, message: `ลบข้อมูลร้าน ${shop} สำเร็จ (${result.count} รายการ)`, count: result.count };
}
catch (error) {
set.status = 500;
return { success: false, message: error.message };
}
})
// ── GET Single Order ──────────────────────────────────────────────────────
.get("/orders/:orderNumber", async ({ params, set }) => {
const order = await prisma.lazadaOrder.findUnique({ where: { orderNumber: params.orderNumber }, include: { items: true } });
if (!order) {
set.status = 404;
return { error: "Order not found" };
}
return order;
})
// ── GET Orders (paginated) ────────────────────────────────────────────────
.get("/orders", async ({ query }) => {
const page = Number(query.page ?? 1);
const limit = Number(query.limit ?? 30);
const skip = (page - 1) * limit;
const shop = query.shop || "hom-aroma";
const where = { shop };
const [items, total] = await Promise.all([
prisma.lazadaOrder.findMany({ where, include: { items: true }, orderBy: { createTime: "desc" }, skip, take: limit }),
prisma.lazadaOrder.count({ where }),
]);
return { items, total, page, limit, totalPages: Math.ceil(total / limit) };
})
// ── GET Summary ───────────────────────────────────────────────────────────
.get("/summary", async ({ query }) => {
const shop = query.shop || "hom-aroma";
const where = { shop };
const [orderCount, agg] = await Promise.all([
prisma.lazadaOrder.count({ where }),
prisma.lazadaOrder.aggregate({ where, _sum: { orderAmount: true, shippingFee: true } }),
]);
return {
orderCount,
totalAmount: agg._sum.orderAmount ?? 0,
totalShipping: agg._sum.shippingFee ?? 0,
};
})
// ── GET Analytics (Single Shop) ───────────────────────────────────────────
.get("/analytics", async ({ query }) => {
const shop = query.shop || "hom-aroma";
const dateFrom = query.dateFrom;
const dateTo = query.dateTo;
const where = { shop };
if (dateFrom || dateTo) {
where.createTime = {};
if (dateFrom)
where.createTime.gte = dateFrom;
if (dateTo)
where.createTime.lte = dateTo + " 23:59";
}
const allOrders = await prisma.lazadaOrder.findMany({
where,
select: {
orderNumber: true, status: true, createTime: true,
orderAmount: true, shippingFee: true, payMethod: true,
items: { select: { itemName: true, variation: true, sellerDiscountTotal: true, platformDiscountTotal: true, paidPrice: true, sellerSku: true } },
},
});
// Monthly aggregation
const monthMap = new Map();
for (const o of allOrders) {
const month = (o.createTime ?? "").slice(0, 7) || "Unknown";
if (!monthMap.has(month))
monthMap.set(month, { month, revenue: 0, orders: 0, cancelled: 0, items: 0, platformDiscount: 0, sellerDiscount: 0 });
const m = monthMap.get(month);
m.orders += 1;
m.revenue += o.orderAmount;
m.platformDiscount += o.items.reduce((s, i) => s + Math.abs(i.platformDiscountTotal), 0);
m.sellerDiscount += o.items.reduce((s, i) => s + Math.abs(i.sellerDiscountTotal), 0);
if (o.status?.toLowerCase().includes("canceled") || o.status?.toLowerCase().includes("failed"))
m.cancelled += 1;
m.items += o.items.length;
}
const monthly = Array.from(monthMap.values()).sort((a, b) => a.month.localeCompare(b.month));
// Status breakdown
const statusMap = new Map();
for (const o of allOrders) {
const key = o.status || "Unknown";
statusMap.set(key, (statusMap.get(key) ?? 0) + 1);
}
const statusBreakdown = Array.from(statusMap.entries()).map(([status, count]) => ({ status, count })).sort((a, b) => b.count - a.count);
// Payment method breakdown
const payMap = new Map();
for (const o of allOrders) {
const key = o.payMethod || "Unknown";
payMap.set(key, (payMap.get(key) ?? 0) + 1);
}
const paymentBreakdown = Array.from(payMap.entries()).map(([method, count]) => ({ method, count })).sort((a, b) => b.count - a.count);
// Top products
const productMap = new Map();
for (const o of allOrders) {
for (const item of o.items) {
const key = `${item.itemName}__${item.variation ?? ""}`;
if (!productMap.has(key))
productMap.set(key, { name: item.itemName, variation: item.variation ?? "", qty: 0, revenue: 0 });
const p = productMap.get(key);
p.qty += 1; // Lazada export is 1 row per item quantity usually, so we add 1
p.revenue += item.paidPrice;
}
}
const topProducts = Array.from(productMap.values()).sort((a, b) => b.revenue - a.revenue).slice(0, 10);
// Summary
const totalRevenue = allOrders.reduce((s, o) => s + o.orderAmount, 0);
const totalShipping = allOrders.reduce((s, o) => s + o.shippingFee, 0);
const totalPlatformDisc = allOrders.flatMap(o => o.items).reduce((s, i) => s + Math.abs(i.platformDiscountTotal), 0);
const totalSellerDisc = allOrders.flatMap(o => o.items).reduce((s, i) => s + Math.abs(i.sellerDiscountTotal), 0);
const totalOrders = allOrders.length;
const cancelledOrders = allOrders.filter(o => o.status?.toLowerCase().includes("canceled")).length;
const cancelRate = totalOrders > 0 ? (cancelledOrders / totalOrders) * 100 : 0;
const completedOrders = totalOrders - cancelledOrders;
const aov = completedOrders > 0 ? totalRevenue / completedOrders : 0;
return {
monthly, statusBreakdown, paymentBreakdown, topProducts,
summary: {
totalRevenue, totalShipping, totalPlatformDisc, totalSellerDisc,
netRevenue: totalRevenue - totalShipping, // Simple net (excluding lazada fees which aren't in this report usually)
totalOrders, completedOrders, cancelledOrders, cancelRate, aov,
},
};
})
// ── GET Analytics Overview (ALL shops combined) ──────────────────────────
.get("/analytics-overview", async ({ query }) => {
const dateFrom = query.dateFrom;
const dateTo = query.dateTo;
const where = {};
if (dateFrom || dateTo) {
where.createTime = {};
if (dateFrom)
where.createTime.gte = dateFrom;
if (dateTo)
where.createTime.lte = dateTo + " 23:59";
}
const allOrders = await prisma.lazadaOrder.findMany({
where,
select: {
orderNumber: true, shop: true, status: true, createTime: true,
orderAmount: true, shippingFee: true,
items: { select: { platformDiscountTotal: true, sellerDiscountTotal: true, paidPrice: true, itemName: true, variation: true } },
},
});
// Per-shop summary
const shopMap = new Map();
for (const o of allOrders) {
if (!shopMap.has(o.shop)) {
shopMap.set(o.shop, { shop: o.shop, orders: 0, completed: 0, cancelled: 0, revenue: 0, shippingFee: 0, platformDiscount: 0, sellerDiscount: 0, items: 0 });
}
const s = shopMap.get(o.shop);
s.orders += 1;
s.revenue += o.orderAmount;
s.shippingFee += o.shippingFee;
s.platformDiscount += o.items.reduce((acc, i) => acc + Math.abs(i.platformDiscountTotal), 0);
s.sellerDiscount += o.items.reduce((acc, i) => acc + Math.abs(i.sellerDiscountTotal), 0);
s.items += o.items.length;
if (o.status?.toLowerCase().includes("canceled"))
s.cancelled += 1;
else
s.completed += 1;
}
const shops = Array.from(shopMap.values());
// Monthly stacked by shop
const monthShopMap = new Map();
for (const o of allOrders) {
const month = (o.createTime ?? "").slice(0, 7) || "Unknown";
if (!monthShopMap.has(month))
monthShopMap.set(month, new Map());
const sm = monthShopMap.get(month);
if (!sm.has(o.shop))
sm.set(o.shop, { revenue: 0, orders: 0 });
const ms = sm.get(o.shop);
ms.revenue += o.orderAmount;
ms.orders += 1;
}
const shopKeys = Array.from(shopMap.keys());
const monthly = Array.from(monthShopMap.entries())
.map(([month, sm]) => {
const row = { month };
for (const shop of shopKeys) {
const d = sm.get(shop) ?? { revenue: 0, orders: 0 };
row[`${shop}_revenue`] = d.revenue;
row[`${shop}_orders`] = d.orders;
}
row.totalRevenue = Array.from(sm.values()).reduce((s, v) => s + v.revenue, 0);
row.totalOrders = Array.from(sm.values()).reduce((s, v) => s + v.orders, 0);
row.revenue = row.totalRevenue; // Provide fallback for global view
return row;
})
.sort((a, b) => a.month.localeCompare(b.month));
// Top Products Global
const productMap = new Map();
for (const o of allOrders) {
for (const item of o.items) {
const key = `${item.itemName}__${item.variation ?? ""}`;
if (!productMap.has(key))
productMap.set(key, { name: item.itemName, variation: item.variation ?? "", qty: 0, revenue: 0 });
const p = productMap.get(key);
p.qty += 1;
p.revenue += item.paidPrice;
}
}
const topProducts = Array.from(productMap.values()).sort((a, b) => b.revenue - a.revenue).slice(0, 10);
// Global summary
const totalRevenue = shops.reduce((s, sh) => s + sh.revenue, 0);
const totalShipping = shops.reduce((s, sh) => s + sh.shippingFee, 0);
const totalPlatformDisc = shops.reduce((s, sh) => s + sh.platformDiscount, 0);
const totalSellerDisc = shops.reduce((s, sh) => s + sh.sellerDiscount, 0);
const totalOrders = shops.reduce((s, sh) => s + sh.orders, 0);
const completedOrders = shops.reduce((s, sh) => s + sh.completed, 0);
const cancelledOrders = shops.reduce((s, sh) => s + sh.cancelled, 0);
return {
shops,
monthly,
topProducts,
summary: {
totalRevenue,
totalShipping,
totalPlatformDisc,
totalSellerDisc,
netRevenue: totalRevenue - totalShipping,
totalOrders,
completedOrders,
cancelledOrders,
cancelRate: totalOrders > 0 ? (cancelledOrders / totalOrders) * 100 : 0,
aov: completedOrders > 0 ? totalRevenue / completedOrders : 0,
}
};
});
//# sourceMappingURL=lazadaOrder.js.map
+60
View File
@@ -0,0 +1,60 @@
import { Elysia } from 'elysia';
import * as line from '@line/bot-sdk';
import { lineConfig, lineClient } from '../lib/lineClient';
import crypto from 'crypto';
const webhookHandler = async ({ request, body, set }) => {
console.log('[LINE Webhook] Handler body type:', typeof body);
console.log('[LINE Webhook] Channel Secret configured:', !!lineConfig.channelSecret, lineConfig.channelSecret ? lineConfig.channelSecret.substring(0, 4) + '...' : 'NONE');
// 1. Verify Signature
const signature = request.headers.get('x-line-signature');
if (!signature) {
set.status = 400;
return { error: 'Missing Signature' };
}
// xxIn our global onParse hook, we intercepted the body and returned it as a raw text string.
const rawBody = typeof body === 'string' ? body : JSON.stringify(body);
if (!rawBody) {
set.status = 400;
return { error: 'Empty Body' };
}
const hash = crypto
.createHmac('SHA256', lineConfig.channelSecret)
.update(rawBody)
.digest('base64');
console.log('[LINE Webhook] Signature Check - Received:', signature, 'Calculated:', hash);
if (hash !== signature) {
console.error('[LINE Webhook] Invalid Signature Blocked');
set.status = 401;
return { error: 'Invalid Signature' };
}
try {
const parsedBody = typeof body === 'string' ? JSON.parse(body) : body;
const events = parsedBody.events || [];
for (const event of events) {
if (event.type === 'message' && event.message.type === 'text') {
// Ignore verification dummy tokens
if (event.replyToken === '00000000000000000000000000000000' || event.replyToken === 'ffffffffffffffffffffffffffffffff') {
continue;
}
// Simple echo bot for now
await lineClient.replyMessage({
replyToken: event.replyToken,
messages: [{
type: 'text',
text: `คุณส่งข้อความว่า: ${event.message.text}`
}]
});
}
}
return { success: true };
}
catch (err) {
console.error('[LINE Webhook] Error:', err);
set.status = 500;
return { error: 'Webhook processing failed' };
}
};
export const lineRoutes = new Elysia({ prefix: '/line' })
.post('/webhook', webhookHandler)
.post('/webhook/', webhookHandler);
//# sourceMappingURL=line.js.map
+77
View File
@@ -0,0 +1,77 @@
import { Elysia } from "elysia";
export declare const mediaRoute: Elysia<"/media", {
decorator: {};
store: {};
derive: {};
resolve: {};
}, {
typebox: {};
error: {};
}, {
schema: {};
standaloneSchema: {};
macro: {};
macroFn: {};
parser: {};
response: {};
}, {
media: {
get: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
} & {
media: {
":id": {
delete: {
body: unknown;
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
200: {
error: string;
success?: never;
message?: never;
} | {
success: boolean;
message: string;
error?: never;
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
};
};
};
};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}>;
//# sourceMappingURL=media.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"media.d.ts","sourceRoot":"","sources":["media.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGhC,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsCnB,CAAC"}
+36
View File
@@ -0,0 +1,36 @@
import { Elysia } from "elysia";
import prisma from "../lib/prisma";
export const mediaRoute = new Elysia({ prefix: "/media" })
.get("/", async () => {
const media = await prisma.media.findMany({
orderBy: { createdAt: "desc" }
});
const BACKEND_PUBLIC_URL = process.env.BACKEND_PUBLIC_URL || "http://localhost:3051";
return media.map(m => ({
...m,
url: `${BACKEND_PUBLIC_URL}${m.path}`
}));
})
.delete("/:id", async ({ params, set }) => {
try {
const mediaId = parseInt(params.id);
const media = await prisma.media.findUnique({
where: { id: mediaId }
});
if (!media) {
set.status = 404;
return { error: "Media not found" };
}
await prisma.media.delete({
where: { id: mediaId }
});
// We are not removing the physical file right now, but could be added later if needed.
return { success: true, message: "Media deleted successfully" };
}
catch (err) {
console.error("[Media] Delete Error:", err);
set.status = 500;
return { error: "Failed to delete media" };
}
});
//# sourceMappingURL=media.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"media.js","sourceRoot":"","sources":["media.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,MAAM,MAAM,eAAe,CAAC;AAEnC,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;KACvD,GAAG,CAAC,GAAG,EAAE,KAAK,IAAI,EAAE;IACnB,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC;QACxC,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;KAC/B,CAAC,CAAC;IAEH,MAAM,kBAAkB,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,uBAAuB,CAAC;IAErF,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACrB,GAAG,CAAC;QACJ,GAAG,EAAE,GAAG,kBAAkB,GAAG,CAAC,CAAC,IAAI,EAAE;KACtC,CAAC,CAAC,CAAC;AACN,CAAC,CAAC;KACD,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,EAAE;IACxC,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAEpC,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC;YAC1C,KAAK,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE;SACvB,CAAC,CAAC;QAEH,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;YACjB,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC;QACtC,CAAC;QAED,MAAM,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;YACxB,KAAK,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE;SACvB,CAAC,CAAC;QAEH,uFAAuF;QAEvF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,4BAA4B,EAAE,CAAC;IAClE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAC;QAC5C,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,wBAAwB,EAAE,CAAC;IAC7C,CAAC;AACH,CAAC,CAAC,CAAC"}
+10
View File
@@ -48,6 +48,16 @@ export declare const menusRoute: Elysia<"/menus", {
put: any;
};
};
} & {
menus: {
items: {
":id": {
translations: {
patch: any;
};
};
};
};
}, {
derive: {};
resolve: {};
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"menus.d.ts","sourceRoot":"","sources":["menus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAInC,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoGnB,CAAC"}
{"version":3,"file":"menus.d.ts","sourceRoot":"","sources":["menus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAInC,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgLnB,CAAC"}
+174
View File
@@ -0,0 +1,174 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
import { adminOnly } from "../middleware/auth";
export const menusRoute = new Elysia({ prefix: "/menus" })
.get("/", async () => {
return await prisma.menu.findMany({
include: {
items: {
orderBy: { order: "asc" },
include: { translations: true }
}
}
});
})
.get("/:name", async ({ params: { name }, set }) => {
const menu = await prisma.menu.findUnique({
where: { name },
include: {
items: {
orderBy: { order: "asc" },
include: { translations: true }
}
}
});
if (!menu) {
return { name, items: [] };
}
return menu;
})
.use(adminOnly)
.post("/", async ({ body, set }) => {
try {
const { name, items } = body;
const menu = await prisma.menu.create({
data: {
name,
items: items ? {
create: items
} : undefined
},
include: { items: true }
});
return menu;
}
catch (err) {
console.error(err);
set.status = 500;
return { error: "Failed to create menu" };
}
}, {
body: t.Object({
name: t.String(),
items: t.Optional(t.Array(t.Object({
label: t.String(),
url: t.String(),
order: t.Number()
})))
})
})
.put("/:name", async ({ params: { name }, body, set }) => {
try {
const { items } = body;
let menu = await prisma.menu.findUnique({ where: { name } });
if (!menu) {
menu = await prisma.menu.create({ data: { name } });
}
await prisma.$transaction(async (tx) => {
const existingItems = await tx.menuItem.findMany({ where: { menuId: menu.id } });
const existingItemIds = existingItems.map((i) => i.id);
const payloadItemIds = items.map(i => i.id).filter(id => id != null);
const itemsToDelete = existingItemIds.filter((id) => !payloadItemIds.includes(id));
if (itemsToDelete.length > 0) {
await tx.menuItem.deleteMany({ where: { id: { in: itemsToDelete } } });
}
for (const item of items) {
if (item.id && existingItemIds.includes(item.id)) {
await tx.menuItem.update({
where: { id: item.id },
data: { label: item.label, url: item.url, order: item.order }
});
if (item.translations?.length) {
for (const t of item.translations) {
await tx.menuItemTranslation.upsert({
where: { menuItemId_langCode: { menuItemId: item.id, langCode: t.langCode } },
update: { label: t.label },
create: { menuItemId: item.id, langCode: t.langCode, label: t.label }
});
}
}
}
else {
await tx.menuItem.create({
data: {
menuId: menu.id,
label: item.label,
url: item.url,
order: item.order,
translations: item.translations?.length ? {
create: item.translations.map(t => ({
langCode: t.langCode,
label: t.label
}))
} : undefined
}
});
}
}
});
return await prisma.menu.findUnique({
where: { name },
include: { items: { orderBy: { order: "asc" }, include: { translations: true } } }
});
}
catch (err) {
console.error(err);
set.status = 500;
return { error: "Failed to update menu" };
}
}, {
body: t.Object({
items: t.Array(t.Object({
id: t.Optional(t.Number()),
label: t.String(),
url: t.String(),
order: t.Number(),
translations: t.Optional(t.Array(t.Object({
langCode: t.String(),
label: t.String()
})))
}))
})
})
.patch("/items/:id/translations", async ({ params, body, set }) => {
const id = Number(params.id);
if (isNaN(id)) {
set.status = 400;
return { error: "Invalid menu item ID" };
}
const menuItem = await prisma.menuItem.findUnique({ where: { id } });
if (!menuItem) {
set.status = 404;
return { error: "Menu item not found. Please ensure database is seeded or the item exists." };
}
const payload = body;
const results = [];
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
for (const lang of LANGS) {
const label = typeof payload.label === 'string' ? (lang === 'th' ? payload.label : null) : payload.label?.[lang];
if (!label)
continue;
const existing = await prisma.menuItemTranslation.findUnique({
where: { menuItemId_langCode: { menuItemId: id, langCode: lang } }
});
if (existing) {
const updated = await prisma.menuItemTranslation.update({
where: { id: existing.id },
data: { label }
});
results.push(updated);
}
else {
const created = await prisma.menuItemTranslation.create({
data: { menuItemId: id, langCode: lang, label }
});
results.push(created);
}
}
const updated = await prisma.menuItem.findUnique({
where: { id },
include: { translations: true }
});
return { success: true, translations: results, menuItem: updated };
});
//# sourceMappingURL=menus.js.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"menus.js","sourceRoot":"","sources":["menus.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"menus.js","sourceRoot":"","sources":["menus.ts"],"names":[],"mappings":"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 truncated
+33
View File
@@ -0,0 +1,33 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const newsletterRoute = new Elysia({ prefix: "/newsletter" })
.post("/subscribe", async ({ body, set }) => {
try {
const { email } = body;
const existing = await prisma.newsletterSubscriber.findUnique({
where: { email }
});
if (existing) {
return { message: "Email already subscribed" };
}
await prisma.newsletterSubscriber.create({
data: { email }
});
return { message: "Successfully subscribed" };
}
catch (err) {
console.error(err);
set.status = 500;
return { error: "Failed to subscribe" };
}
}, {
body: t.Object({
email: t.String({ format: "email" })
})
})
.get("/subscribers", async () => {
return await prisma.newsletterSubscriber.findMany({
orderBy: { createdAt: "desc" }
});
});
//# sourceMappingURL=newsletter.js.map
+247
View File
@@ -0,0 +1,247 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
import { sendOrderCreatedNotification, sendPaymentConfirmedNotification, sendOrderShippedNotification, sendOrderCancelledNotification, markOrderAsPaid } from "../lib/notifications";
export const ordersRoute = new Elysia({ prefix: "/orders" })
.get("/", async () => {
return await prisma.order.findMany({
orderBy: { createdAt: 'desc' },
include: { items: true, customer: true }
});
})
.get("/:id", async ({ params: { id }, set }) => {
const order = await prisma.order.findUnique({
where: { id: Number(id) },
include: { items: true, customer: true }
});
if (!order) {
set.status = 404;
return { error: "Order not found" };
}
return order;
})
.post("/", async ({ body, set }) => {
try {
const data = body;
const settings = await prisma.storeSettings.findFirst();
const earnRate = settings?.pointsEarnRate || 1.0;
const netAmount = data.totalAmount - data.shippingFee;
const pointsEarned = Math.floor(Math.max(0, netAmount) * earnRate);
if (data.customerId && data.pointsUsed && data.pointsUsed > 0) {
// Verify customer has enough points
const customer = await prisma.customer.findUnique({ where: { id: data.customerId } });
if (!customer || customer.points < data.pointsUsed) {
set.status = 400;
return { error: "Insufficient points" };
}
// Deduct points immediately
await prisma.customer.update({
where: { id: data.customerId },
data: { points: { decrement: data.pointsUsed } }
});
}
const order = await prisma.order.create({
data: {
customerId: data.customerId,
customerName: data.customerName,
customerEmail: data.customerEmail,
customerPhone: data.customerPhone,
shippingAddress: data.shippingAddress,
subtotal: data.subtotal || 0,
promoLevel1Name: data.promoLevel1Name,
promoLevel1Disc: data.promoLevel1Disc || 0,
promoLevel2Name: data.promoLevel2Name,
promoLevel2Disc: data.promoLevel2Disc || 0,
couponCode: data.couponCode || null,
discountAmount: data.discountAmount || 0,
pointsUsed: data.pointsUsed || 0,
pointsDiscount: data.pointsDiscount || 0,
pointsEarned: pointsEarned,
shippingFee: data.shippingFee,
totalAmount: data.totalAmount,
items: {
create: data.items.map((item) => ({
productId: item.productId,
variantId: item.variantId,
quantity: item.quantity,
priceAtPurchase: item.priceAtPurchase,
productName: item.productName,
variantSize: item.variantSize,
productImage: item.productImage
}))
}
},
include: { items: true }
});
// If a coupon was used, increment its usedCount
if (data.couponCode) {
try {
await prisma.coupon.update({
where: { code: data.couponCode.toUpperCase() },
data: { usedCount: { increment: 1 } }
});
}
catch (e) {
console.error("Failed to increment coupon usedCount:", e);
}
}
// Trigger LINE notification asynchronously
sendOrderCreatedNotification(order.id).catch(e => {
console.error("Failed to trigger sendOrderCreatedNotification:", e);
});
return order;
}
catch (err) {
console.error("[Orders] Error:", err);
set.status = 500;
return { error: "Failed to create order" };
}
}, {
body: t.Object({
customerId: t.Optional(t.Nullable(t.Number())),
customerName: t.String(),
customerEmail: t.String(),
customerPhone: t.String(),
shippingAddress: t.String(),
totalAmount: t.Number(),
shippingFee: t.Number(),
subtotal: t.Optional(t.Number()),
promoLevel1Name: t.Optional(t.Nullable(t.String())),
promoLevel1Disc: t.Optional(t.Number()),
promoLevel2Name: t.Optional(t.Nullable(t.String())),
promoLevel2Disc: t.Optional(t.Number()),
couponCode: t.Optional(t.Nullable(t.String())),
discountAmount: t.Optional(t.Number()),
pointsUsed: t.Optional(t.Number()),
pointsDiscount: t.Optional(t.Number()),
items: t.Array(t.Object({
productId: t.Number(),
variantId: t.Optional(t.Number()),
quantity: t.Number(),
priceAtPurchase: t.Number(),
productName: t.String(),
variantSize: t.Optional(t.String()),
productImage: t.Optional(t.String())
}))
})
})
.patch("/:id/status", async ({ params: { id }, body, set }) => {
try {
const { status } = body;
const order = await prisma.order.findUnique({ where: { id: Number(id) } });
if (!order) {
set.status = 404;
return { error: "Order not found" };
}
if (status === 'PAID') {
await markOrderAsPaid(order.id);
}
else {
await prisma.order.update({
where: { id: order.id },
data: { status }
});
// If status becomes CANCELLED, refund the points used and notify
if (status === 'CANCELLED' && order.status !== 'CANCELLED') {
if (order.customerId && order.pointsUsed > 0) {
await prisma.customer.update({
where: { id: order.customerId },
data: { points: { increment: order.pointsUsed } }
});
}
sendOrderCancelledNotification(order.id).catch(e => console.error(e));
}
// If status becomes SHIPPED, notify
if (status === 'SHIPPED' && order.status !== 'SHIPPED') {
sendOrderShippedNotification(order.id).catch(e => console.error(e));
}
}
return await prisma.order.findUnique({
where: { id: order.id },
include: { items: true, customer: true }
});
}
catch (err) {
set.status = 500;
return { error: "Failed to update order status" };
}
}, {
body: t.Object({
status: t.String()
})
})
.patch("/:id/slip", async ({ params: { id }, body, set }) => {
try {
const { paymentSlipUrl } = body;
return await prisma.order.update({
where: { id: Number(id) },
data: { paymentSlipUrl },
include: { items: true }
});
}
catch (err) {
set.status = 500;
return { error: "Failed to upload slip" };
}
}, {
body: t.Object({
paymentSlipUrl: t.String()
})
})
.patch("/:id/tracking", async ({ params: { id }, body, set }) => {
try {
const { trackingNo } = body;
const order = await prisma.order.update({
where: { id: Number(id) },
data: { trackingNo, status: "SHIPPED" },
include: { items: true }
});
// Trigger notification asynchronously
sendOrderShippedNotification(order.id).catch(e => {
console.error("Failed to trigger sendOrderShippedNotification:", e);
});
return order;
}
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) },
orderBy: { createdAt: 'desc' },
include: { items: true }
});
})
.post("/track", async ({ body, set }) => {
try {
const { orderId, phone } = body;
const order = await prisma.order.findUnique({
where: { id: Number(orderId) }
});
if (!order) {
set.status = 404;
return { error: "Order not found" };
}
const sanitizePhone = (p) => p.replace(/[^0-9]/g, '');
if (sanitizePhone(order.customerPhone) !== sanitizePhone(phone)) {
set.status = 401;
return { error: "Authentication failed. Phone number does not match." };
}
return { success: true, orderId: order.id };
}
catch (err) {
set.status = 500;
return { error: "Invalid tracking request" };
}
}, {
body: t.Object({
orderId: t.Number(),
phone: t.String()
})
});
//# sourceMappingURL=orders.js.map
+8
View File
@@ -107,6 +107,14 @@ export declare const pagesRoute: Elysia<"/pages", {
delete: any;
};
};
} & {
pages: {
":id": {
translations: {
patch: any;
};
};
};
} & {
pages: {
":slug": {
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["pages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAkBnC,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA0UnB,CAAC"}
{"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["pages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AA8BnC,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAkXnB,CAAC"}
+482
View File
@@ -0,0 +1,482 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
import { HeroSectionPayloadSchema, CategoriesSectionPayloadSchema, ProductsSectionPayloadSchema, BannerSectionPayloadSchema, TextSectionPayloadSchema, AnnouncementBarSectionPayloadSchema, NewsletterSectionPayloadSchema, EditorialTileSectionPayloadSchema, EditorialCarouselSectionPayloadSchema, HeroArticlePayloadSchema, RichTextPayloadSchema, ScentNotesBreakdownPayloadSchema, QuoteCalloutPayloadSchema, ProductCarouselPayloadSchema, AccordionSectionPayloadSchema, ContactInfoSectionPayloadSchema, ContactFormSectionPayloadSchema } from "@homaroma/types";
import { adminOnly } from "../middleware/auth";
const RESERVED_SLUGS = [
"shop", "category", "products", "cart", "checkout", "account", "search",
"login", "admin", "wishlist", "track", "coupons", "scent-guide",
"profile", "article", "pages", "orders", "customers",
"settings", "menus", "auth", "api"
];
export const pagesRoute = new Elysia({ prefix: "/pages" })
.get("/", async () => {
return await prisma.page.findMany({
include: {
sections: {
orderBy: { order: "asc" }
}
}
});
})
.get("/:slug", async ({ params: { slug }, query, set }) => {
const includeInactive = String(query?.includeInactive || "").toLowerCase() === "true";
const lang = query?.lang || 'th';
const page = await prisma.page.findUnique({
where: { slug },
include: {
sections: {
where: includeInactive ? undefined : { 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;
if (!payload)
return section;
const populatedPayload = { ...payload };
if (section.type === 'PRODUCTS') {
let products = [];
const limit = payload.limit || 3;
const productInclude = {
category: true,
scent: true,
translations: true,
variants: {
include: {
bomItems: { include: { rawMaterial: true } }
},
orderBy: { price: "asc" }
},
images: {
orderBy: { order: "asc" }
}
};
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.map((p) => {
if (lang === 'all') {
const nameObj = {};
const descObj = {};
const detailsObj = {};
p.translations?.forEach((t) => {
nameObj[t.langCode] = t.name;
descObj[t.langCode] = t.description;
detailsObj[t.langCode] = t.details;
});
return { ...p, name: nameObj, description: descObj, details: detailsObj };
}
else {
const trans = p.translations?.find((t) => t.langCode === lang) || p.translations?.find((t) => t.langCode === 'th') || p.translations?.[0];
return { ...p, name: trans?.name || p.scent?.name || '', description: trans?.description, details: trans?.details };
}
});
}
else if (section.type === 'CATEGORIES') {
let categories = [];
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
};
})
.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("/newsletter", async ({ body, set }) => {
try {
const { email } = body;
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()
})
})
.use(adminOnly)
.post("/", async ({ body, set }) => {
try {
const data = body;
const { id, slug, title, isPublished, status, metadata } = data;
if (typeof slug === 'string' && RESERVED_SLUGS.includes(slug.toLowerCase())) {
set.status = 400;
return { error: "Reserved slug cannot be used" };
}
let finalIsPublished = true;
if (status !== undefined) {
finalIsPublished = status === "published";
}
else if (isPublished !== undefined) {
finalIsPublished = isPublished;
}
if (id) {
return await prisma.page.update({
where: { id: Number(id) },
data: { slug, title, isPublished: finalIsPublished }
});
}
else {
return await prisma.page.create({
data: { slug, title, isPublished: finalIsPublished }
});
}
}
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()),
status: t.Optional(t.String()),
metadata: t.Optional(t.Any())
})
})
.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" };
}
})
.patch("/:id/translations", async ({ params, body, set }) => {
const id = Number(params.id);
if (isNaN(id)) {
set.status = 400;
return { error: "Invalid page ID" };
}
const payload = body;
const results = [];
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
for (const lang of LANGS) {
const title = typeof payload.title === 'string' ? (lang === 'th' ? payload.title : null) : payload.title?.[lang];
if (!title)
continue;
const existing = await prisma.pageTranslation.findUnique({
where: { pageId_langCode: { pageId: id, langCode: lang } }
});
if (existing) {
const updated = await prisma.pageTranslation.update({
where: { id: existing.id },
data: { title }
});
results.push(updated);
}
else {
const created = await prisma.pageTranslation.create({
data: { pageId: id, langCode: lang, title }
});
results.push(created);
}
}
const updated = await prisma.page.findUnique({
where: { id },
include: { translations: true }
});
return { success: true, translations: results, page: updated };
})
.post("/:slug/sections", async ({ params: { slug }, body, set }) => {
try {
const pageId = Number(slug);
const { type, order, payload, isActive } = body;
const validationError = validateSectionPayload(type, payload);
if (validationError) {
set.status = 400;
return { error: validationError };
}
let finalOrder = order;
if (finalOrder === undefined) {
const lastSection = await prisma.pageSection.findFirst({
where: { pageId },
orderBy: { order: "desc" }
});
finalOrder = lastSection ? lastSection.order + 1 : 1;
}
return await prisma.pageSection.create({
data: {
pageId,
type: type.toUpperCase(),
order: finalOrder,
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.Optional(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;
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;
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()
}))
})
})
.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, payload) {
if (!payload || typeof payload !== 'object') {
return "Payload must be a JSON object";
}
switch (type.toUpperCase()) {
case 'HERO': {
const res = HeroSectionPayloadSchema.safeParse(payload);
if (!res.success)
return res.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'CATEGORIES': {
const res = CategoriesSectionPayloadSchema.safeParse(payload);
if (!res.success)
return res.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'PRODUCTS': {
const res = ProductsSectionPayloadSchema.safeParse(payload);
if (!res.success)
return res.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'BANNER': {
const res = BannerSectionPayloadSchema.safeParse(payload);
if (!res.success)
return res.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'TEXT': {
const res = TextSectionPayloadSchema.safeParse(payload);
if (!res.success)
return res.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'ANNOUNCEMENT_BAR': {
const res = AnnouncementBarSectionPayloadSchema.safeParse(payload);
if (!res.success)
return res.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'NEWSLETTER': {
const res = NewsletterSectionPayloadSchema.safeParse(payload);
if (!res.success)
return res.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'EDITORIAL_TILE': {
const res = EditorialTileSectionPayloadSchema.safeParse(payload);
if (!res.success)
return res.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'EDITORIAL_CAROUSEL': {
const res = EditorialCarouselSectionPayloadSchema.safeParse(payload);
if (!res.success)
return res.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'HERO_ARTICLE': {
const res = HeroArticlePayloadSchema.safeParse(payload);
if (!res.success)
return res.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'RICH_TEXT': {
const res = RichTextPayloadSchema.safeParse(payload);
if (!res.success)
return res.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'SCENT_NOTES_BREAKDOWN': {
const res = ScentNotesBreakdownPayloadSchema.safeParse(payload);
if (!res.success)
return res.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'QUOTE_CALLOUT': {
const res = QuoteCalloutPayloadSchema.safeParse(payload);
if (!res.success)
return res.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'PRODUCT_CAROUSEL': {
const res = ProductCarouselPayloadSchema.safeParse(payload);
if (!res.success)
return res.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'ACCORDION': {
const res = AccordionSectionPayloadSchema.safeParse(payload);
if (!res.success)
return res.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'CONTACT_INFO': {
const res = ContactInfoSectionPayloadSchema.safeParse(payload);
if (!res.success)
return res.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'CONTACT_FORM': {
const res = ContactFormSectionPayloadSchema.safeParse(payload);
if (!res.success)
return res.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
default:
return `Unknown section type: ${type}`;
}
return null;
}
//# sourceMappingURL=pages.js.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"pages.js","sourceRoot":"","sources":["pages.ts"],"names":[],"mappings":"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 truncated
{"version":3,"file":"pages.js","sourceRoot":"","sources":["pages.ts"],"names":[],"mappings":"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 truncated
+152
View File
@@ -0,0 +1,152 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
import { markOrderAsPaid } from "../lib/notifications";
const OMISE_SECRET_KEY = process.env.OMISE_SECRET_KEY || "";
export const paymentsRoute = new Elysia({ prefix: "/payments" })
.post("/charge", async ({ body, set }) => {
console.log("[Payment Charge] Incoming request body:", JSON.stringify(body));
try {
const { orderId, amount, type, token } = body;
if (!OMISE_SECRET_KEY) {
console.error("[Payment Charge] Error: OMISE_SECRET_KEY is empty");
set.status = 500;
return { error: "Omise secret key is not configured" };
}
// 1. Check if order exists
const order = await prisma.order.findUnique({
where: { id: orderId }
});
if (!order) {
set.status = 404;
return { error: "Order not found" };
}
// Convert amount to satang (integer)
const amountInSatang = Math.round(amount * 100);
// Setup Basic Auth header for Omise
const authHeader = "Basic " + Buffer.from(OMISE_SECRET_KEY + ":").toString("base64");
if (type === "card") {
if (!token) {
set.status = 400;
return { error: "Card token is required for card payments" };
}
// Create a card charge directly
const chargeRes = await fetch("https://api.omise.co/charges", {
method: "POST",
headers: {
"Authorization": authHeader,
"Content-Type": "application/json"
},
body: JSON.stringify({
amount: amountInSatang,
currency: "THB",
card: token,
metadata: {
orderId: String(orderId)
}
})
});
const charge = await chargeRes.json();
if (charge.error) {
set.status = 400;
return { error: charge.message || "Failed to create credit card charge" };
}
// If card payment is immediately successful (some cards don't require 3DS)
if (charge.status === "successful") {
await markOrderAsPaid(orderId);
}
return {
success: true,
status: charge.status,
chargeId: charge.id,
authorizeUri: charge.authorize_uri || null // For 3DS card validation redirect
};
}
else {
// PromptPay:
// 1. Create source
const sourceRes = await fetch("https://api.omise.co/sources", {
method: "POST",
headers: {
"Authorization": authHeader,
"Content-Type": "application/json"
},
body: JSON.stringify({
type: "promptpay",
amount: amountInSatang,
currency: "THB"
})
});
const source = await sourceRes.json();
console.log("[Payment Charge] Omise PromptPay Source response:", JSON.stringify(source));
if (source.error) {
set.status = 400;
return { error: source.message || "Failed to create PromptPay source" };
}
// 2. Create charge
const chargeRes = await fetch("https://api.omise.co/charges", {
method: "POST",
headers: {
"Authorization": authHeader,
"Content-Type": "application/json"
},
body: JSON.stringify({
amount: amountInSatang,
currency: "THB",
source: source.id,
metadata: {
orderId: String(orderId)
}
})
});
const charge = await chargeRes.json();
console.log("[Payment Charge] Omise PromptPay Charge response:", JSON.stringify(charge));
if (charge.error) {
set.status = 400;
return { error: charge.message || "Failed to create PromptPay charge" };
}
// Get QR Code Image URL from scannable_code or references
const qrCodeUrl = charge.source?.scannable_code?.image?.download_uri || charge.source?.references?.barcode || null;
return {
success: true,
status: charge.status,
chargeId: charge.id,
qrCodeUrl: qrCodeUrl
};
}
}
catch (err) {
console.error("[Payment Charge] Error:", err);
set.status = 500;
return { error: err.message || "Failed to process payment request" };
}
}, {
body: t.Object({
orderId: t.Number(),
amount: t.Number(),
type: t.Union([t.Literal("card"), t.Literal("promptpay")]),
token: t.Optional(t.String())
})
})
.post("/webhook", async ({ body, set }) => {
try {
const event = body;
console.log(`[Omise Webhook] Received event: ${event.key}`);
if (event.key === "charge.complete") {
const charge = event.data;
const status = charge.status;
const orderIdStr = charge.metadata?.orderId;
if (orderIdStr && status === "successful") {
const orderId = parseInt(orderIdStr, 10);
console.log(`[Omise Webhook] Payment successful for Order #${orderId}`);
await markOrderAsPaid(orderId);
}
}
return { received: true };
}
catch (err) {
console.error("[Omise Webhook] Error processing event:", err);
set.status = 500;
return { error: "Failed to process webhook" };
}
});
//# sourceMappingURL=payments.js.map
+36
View File
@@ -30,6 +30,42 @@ export declare const productsRoute: Elysia<"/products", {
};
};
};
} & {
products: {
scents: {
":id": {
translations: {
patch: {
body: unknown;
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
200: {
success: boolean;
translations: any[];
scent: any;
};
400: {
readonly message: "Invalid scent ID";
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
};
};
};
};
};
};
} & {
products: {
get: {
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"products.d.ts","sourceRoot":"","sources":["products.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAGnC,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8RvB,CAAC"}
{"version":3,"file":"products.d.ts","sourceRoot":"","sources":["products.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAGnC,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoUvB,CAAC"}
+314
View File
@@ -0,0 +1,314 @@
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" },
include: { translations: true }
});
})
.patch("/scents/:id/translations", async ({ params, body, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid scent ID" });
const payload = body;
const results = [];
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
for (const lang of LANGS) {
const name = typeof payload.name === 'string' ? (lang === 'th' ? payload.name : null) : payload.name?.[lang];
if (!name)
continue;
const existing = await prisma.scentTranslation.findUnique({
where: { scentId_langCode: { scentId: id, langCode: lang } }
});
if (existing) {
const updated = await prisma.scentTranslation.update({
where: { id: existing.id },
data: { name },
});
results.push(updated);
}
else {
const created = await prisma.scentTranslation.create({
data: { scentId: id, langCode: lang, name },
});
results.push(created);
}
}
const updated = await prisma.scent.findUnique({
where: { id },
include: { translations: true },
});
return { success: true, translations: results, scent: updated };
})
.get("/", async ({ query }) => {
const categoryId = query.categoryId ? parseInt(query.categoryId) : undefined;
const scentId = query.scentId ? parseInt(query.scentId) : undefined;
const search = query.search?.trim() || undefined;
const lang = query.lang || 'th';
// Pagination params
const page = query.page ? parseInt(query.page) : 1;
const limit = query.limit ? parseInt(query.limit) : 12;
const skip = (page - 1) * limit;
const where = {
...(categoryId && { categoryId }),
...(scentId && { scentId }),
...(search && {
OR: [
{ translations: { some: { name: { contains: search, mode: "insensitive" } } } },
{ scent: { name: { contains: search, mode: "insensitive" } } },
{ category: { name: { contains: search, mode: "insensitive" } } },
],
}),
};
const [products, total] = await Promise.all([
prisma.product.findMany({
where,
include: {
category: { include: { translations: true } },
scent: true,
translations: true,
variants: {
include: {
bomItems: { include: { rawMaterial: true } }
},
orderBy: { price: "asc" }
},
images: { orderBy: { order: "asc" } },
},
skip,
take: limit,
}),
prisma.product.count({ where }),
]);
// Map translations
const mappedProducts = products.map(p => {
if (lang === 'all') {
const nameObj = {};
const descObj = {};
const detailsObj = {};
p.translations.forEach(t => {
nameObj[t.langCode] = t.name;
descObj[t.langCode] = t.description;
detailsObj[t.langCode] = t.details;
});
return { ...p, name: nameObj, description: descObj, details: detailsObj, category: { ...p.category, name: p.category.name } }; // Actually for 'all', keep as is, but wait, category name needs resolving if we want it? Let's just resolve it simply below.
}
else {
const trans = p.translations.find(t => t.langCode === lang) || p.translations.find(t => t.langCode === 'th') || p.translations[0];
const catTrans = p.category.translations?.find((t) => t.langCode === lang) || p.category.translations?.find((t) => t.langCode === 'th') || p.category.translations?.[0];
return {
...p,
name: trans?.name || '',
description: trans?.description,
details: trans?.details,
category: { ...p.category, name: catTrans?.name || p.category.name }
};
}
});
mappedProducts.sort((a, b) => {
const aName = typeof a.name === 'string' ? a.name : (a.name?.th || '');
const bName = typeof b.name === 'string' ? b.name : (b.name?.th || '');
return aName.localeCompare(bName);
});
return {
data: mappedProducts,
meta: {
total,
page,
limit,
lastPage: Math.ceil(total / limit),
},
};
})
.get("/:id", async ({ params, query, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid product ID" });
const lang = query.lang || 'th';
const product = await prisma.product.findUnique({
where: { id },
include: {
category: { include: { translations: true } },
scent: true,
translations: true,
variants: {
include: {
bomItems: { include: { rawMaterial: true } }
},
orderBy: { price: "asc" }
},
images: { orderBy: { order: "asc" } },
},
});
if (!product)
return status(404, { message: "Product not found" });
if (lang === 'all') {
const nameObj = {};
const descObj = {};
const detailsObj = {};
product.translations.forEach(t => {
nameObj[t.langCode] = t.name;
descObj[t.langCode] = t.description;
detailsObj[t.langCode] = t.details;
});
return { ...product, name: nameObj, description: descObj, details: detailsObj };
}
else {
const trans = product.translations.find(t => t.langCode === lang) || product.translations.find(t => t.langCode === 'th') || product.translations[0];
const catTrans = product.category.translations?.find((t) => t.langCode === lang) || product.category.translations?.find((t) => t.langCode === 'th') || product.category.translations?.[0];
return {
...product,
name: trans?.name || '',
description: trans?.description,
details: trans?.details,
category: { ...product.category, name: catTrans?.name || product.category.name }
};
}
})
.put("/:id", async ({ params, body, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid product ID" });
const { categoryId, description, details, variants, images } = body;
try {
const updatedProduct = await prisma.$transaction(async (tx) => {
// 1. Update basic product info
const prod = await tx.product.update({
where: { id },
data: { categoryId },
});
// 2. Sync Variants
await tx.productVariant.deleteMany({
where: { productId: id },
});
if (variants && variants.length > 0) {
for (const v of variants) {
await tx.productVariant.create({
data: {
productId: id,
size: v.size,
price: v.price,
bomItems: v.bomItems && v.bomItems.length > 0 ? {
create: v.bomItems.map((b) => ({
rawMaterialId: b.rawMaterialId,
quantity: b.quantity,
}))
} : undefined
}
});
}
}
// 3. Sync Images
await tx.productImage.deleteMany({
where: { productId: id },
});
if (images && images.length > 0) {
for (const [idx, img] of images.entries()) {
await tx.productImage.create({
data: {
productId: id,
url: img.url,
order: img.order ?? (typeof img.order === 'number' ? img.order : idx),
}
});
}
}
return prod;
});
return { success: true, product: updatedProduct };
}
catch (err) {
console.error("Update product error:", err);
return status(500, { success: false, message: "Failed to update product" });
}
}, {
body: t.Any(),
detail: {
summary: "Update Product",
tags: ["products"],
},
})
.patch("/:id", async ({ params, body, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid product ID" });
try {
const payload = body;
const prodData = {};
if (payload.categoryId !== undefined)
prodData.categoryId = payload.categoryId;
if (payload.scentId !== undefined)
prodData.scentId = payload.scentId;
if (payload.price !== undefined)
prodData.price = payload.price;
const prod = await prisma.product.update({
where: { id },
data: prodData,
});
// Update translations
// If name, description, details are strings, treat as 'th'
// If they are objects, iterate over them
const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
for (const lang of languages) {
const updateData = {};
let shouldUpdate = false;
if (payload.name) {
const val = typeof payload.name === 'string' ? (lang === 'th' ? payload.name : null) : payload.name[lang];
if (val !== undefined && val !== null) {
updateData.name = val;
shouldUpdate = true;
}
}
if (payload.description) {
const val = typeof payload.description === 'string' ? (lang === 'th' ? payload.description : null) : payload.description[lang];
if (val !== undefined && val !== null) {
updateData.description = val;
shouldUpdate = true;
}
}
if (payload.details) {
const val = typeof payload.details === 'string' ? (lang === 'th' ? payload.details : null) : payload.details[lang];
if (val !== undefined && val !== null) {
updateData.details = val;
shouldUpdate = true;
}
}
if (shouldUpdate) {
// First check if translation exists to create or update
const existing = await prisma.productTranslation.findUnique({
where: { productId_langCode: { productId: id, langCode: lang } }
});
if (existing) {
await prisma.productTranslation.update({
where: { id: existing.id },
data: updateData
});
}
else if (updateData.name) { // name is required for creation
await prisma.productTranslation.create({
data: {
productId: id,
langCode: lang,
name: updateData.name,
description: updateData.description,
details: updateData.details
}
});
}
}
}
return { success: true, product: prod };
}
catch (err) {
console.error("Patch product error:", err);
return status(500, { success: false, message: "Failed to patch product" });
}
}, {
body: t.Any(),
detail: {
summary: "Partial Update Product",
tags: ["products"],
},
});
//# sourceMappingURL=products.js.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"products.js","sourceRoot":"","sources":["products.ts"],"names":[],"mappings":"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 truncated
{"version":3,"file":"products.js","sourceRoot":"","sources":["products.ts"],"names":[],"mappings":"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 truncated
+161
View File
@@ -0,0 +1,161 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const profileRoute = new Elysia({ prefix: "/profile" })
.get("/:customerId", async ({ params: { customerId }, set }) => {
const customer = await prisma.customer.findUnique({
where: { id: Number(customerId) },
include: { addresses: true }
});
if (!customer) {
set.status = 404;
return { error: "Customer not found" };
}
return customer;
})
.put("/:customerId", async ({ params: { customerId }, body, set }) => {
try {
const { email, phone } = body;
const customer = await prisma.customer.update({
where: { id: Number(customerId) },
data: {
email: email || null,
phone: phone || null
}
});
return customer;
}
catch (err) {
set.status = 500;
return { error: err.message || "Failed to update customer profile" };
}
}, {
body: t.Object({
email: t.Optional(t.Nullable(t.String())),
phone: t.Optional(t.Nullable(t.String()))
})
})
.get("/:customerId/addresses", async ({ params: { customerId } }) => {
return await prisma.customerAddress.findMany({
where: { customerId: Number(customerId) },
orderBy: { isDefault: 'desc' }
});
})
.post("/:customerId/addresses", async ({ params: { customerId }, body, set }) => {
try {
const { recipientName, phone, addressLine, subdistrict, district, province, zipcode, isDefault } = body;
const cId = Number(customerId);
// If isDefault is true, unset other defaults first
if (isDefault) {
await prisma.customerAddress.updateMany({
where: { customerId: cId },
data: { isDefault: false }
});
}
const address = await prisma.customerAddress.create({
data: {
customerId: cId,
recipientName,
phone,
addressLine,
subdistrict,
district,
province,
zipcode,
isDefault: !!isDefault
}
});
return address;
}
catch (err) {
set.status = 500;
return { error: err.message || "Failed to create address" };
}
}, {
body: t.Object({
recipientName: t.String(),
phone: t.String(),
addressLine: t.String(),
subdistrict: t.String(),
district: t.String(),
province: t.String(),
zipcode: t.String(),
isDefault: t.Boolean()
})
})
.put("/:customerId/addresses/:addressId", async ({ params: { customerId, addressId }, body, set }) => {
try {
const { recipientName, phone, addressLine, subdistrict, district, province, zipcode, isDefault } = body;
const cId = Number(customerId);
const aId = Number(addressId);
// If setting as default, unset other defaults first
if (isDefault) {
await prisma.customerAddress.updateMany({
where: { customerId: cId },
data: { isDefault: false }
});
}
const address = await prisma.customerAddress.update({
where: { id: aId },
data: {
recipientName,
phone,
addressLine,
subdistrict,
district,
province,
zipcode,
isDefault: !!isDefault
}
});
return address;
}
catch (err) {
set.status = 500;
return { error: err.message || "Failed to update address" };
}
}, {
body: t.Object({
recipientName: t.String(),
phone: t.String(),
addressLine: t.String(),
subdistrict: t.String(),
district: t.String(),
province: t.String(),
zipcode: t.String(),
isDefault: t.Boolean()
})
})
.delete("/:customerId/addresses/:addressId", async ({ params: { addressId }, set }) => {
try {
await prisma.customerAddress.delete({
where: { id: Number(addressId) }
});
return { success: true };
}
catch (err) {
set.status = 500;
return { error: err.message || "Failed to delete address" };
}
})
.patch("/:customerId/addresses/:addressId/default", async ({ params: { customerId, addressId }, set }) => {
try {
const cId = Number(customerId);
const aId = Number(addressId);
// Unset other defaults
await prisma.customerAddress.updateMany({
where: { customerId: cId },
data: { isDefault: false }
});
// Set this one as default
const address = await prisma.customerAddress.update({
where: { id: aId },
data: { isDefault: true }
});
return address;
}
catch (err) {
set.status = 500;
return { error: err.message || "Failed to set default address" };
}
});
//# sourceMappingURL=profile.js.map
+82
View File
@@ -0,0 +1,82 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const promotionsRoute = new Elysia({ prefix: "/promotions" })
.get("/active", async () => {
const now = new Date();
return await prisma.promotion.findMany({
where: {
isActive: true,
OR: [
{ startDate: null },
{ startDate: { lte: now } }
],
AND: [
{ OR: [{ endDate: null }, { endDate: { gte: now } }] }
]
},
orderBy: { level: 'asc' }
});
})
.get("/", async () => {
return await prisma.promotion.findMany({
orderBy: [{ level: 'asc' }, { createdAt: 'desc' }]
});
})
.post("/", async ({ body, set }) => {
try {
const data = body;
return await prisma.promotion.create({ data });
}
catch (err) {
set.status = 500;
return { error: "Failed to create promotion" };
}
}, {
body: t.Object({
name: t.String(),
level: t.Number(),
type: t.String(),
value: t.Number(),
minPurchase: t.Number(),
isActive: t.Boolean(),
startDate: t.Optional(t.Nullable(t.String())),
endDate: t.Optional(t.Nullable(t.String()))
})
})
.put("/:id", async ({ params: { id }, body, set }) => {
try {
const data = body;
return await prisma.promotion.update({
where: { id: Number(id) },
data
});
}
catch (err) {
set.status = 500;
return { error: "Failed to update promotion" };
}
}, {
body: t.Object({
name: t.String(),
level: t.Number(),
type: t.String(),
value: t.Number(),
minPurchase: t.Number(),
isActive: t.Boolean(),
startDate: t.Optional(t.Nullable(t.String())),
endDate: t.Optional(t.Nullable(t.String()))
})
})
.delete("/:id", async ({ params: { id }, set }) => {
try {
await prisma.promotion.delete({
where: { id: Number(id) }
});
return { success: true };
}
catch (err) {
set.status = 500;
return { error: "Failed to delete promotion" };
}
});
//# sourceMappingURL=promotions.js.map
+80
View File
@@ -0,0 +1,80 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const rawMaterialsRoute = new Elysia({ prefix: "/raw-materials" })
.get("/", async () => {
const rawMaterials = await prisma.rawMaterial.findMany({
orderBy: { name: "asc" },
});
return rawMaterials;
})
.get("/:id", async ({ params, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid ID" });
const rawMaterial = await prisma.rawMaterial.findUnique({
where: { id },
});
if (!rawMaterial)
return status(404, { message: "Raw material not found" });
return rawMaterial;
})
.post("/", async ({ body, status }) => {
try {
const rawMaterial = await prisma.rawMaterial.create({
data: body,
});
return status(201, rawMaterial);
}
catch (err) {
console.error(err);
return status(500, { message: "Failed to create raw material" });
}
}, {
body: t.Object({
name: t.String(),
unit: t.String(),
pricePerUnit: t.Number(),
}),
detail: { tags: ["raw-materials"] }
})
.put("/:id", async ({ params, body, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid ID" });
try {
const rawMaterial = await prisma.rawMaterial.update({
where: { id },
data: body,
});
return rawMaterial;
}
catch (err) {
console.error(err);
return status(500, { message: "Failed to update raw material" });
}
}, {
body: t.Object({
name: t.String(),
unit: t.String(),
pricePerUnit: t.Number(),
}),
detail: { tags: ["raw-materials"] }
})
.delete("/:id", async ({ params, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid ID" });
try {
await prisma.rawMaterial.delete({
where: { id },
});
return { success: true };
}
catch (err) {
console.error(err);
return status(500, { message: "Failed to delete raw material" });
}
}, {
detail: { tags: ["raw-materials"] }
});
//# sourceMappingURL=raw-materials.js.map
+50
View File
@@ -0,0 +1,50 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const reviewsRoute = new Elysia({ prefix: "/reviews" })
.get("/product/:productId", async ({ params: { productId } }) => {
return await prisma.review.findMany({
where: {
productId: Number(productId),
isPublished: true
},
orderBy: { createdAt: 'desc' },
include: {
customer: {
select: {
displayName: true,
pictureUrl: true
}
}
}
});
})
.post("/", async ({ body, set }) => {
try {
const data = body;
const review = await prisma.review.create({
data: {
rating: data.rating,
comment: data.comment,
productId: data.productId,
customerId: data.customerId || null,
customerName: data.customerName || "Anonymous",
isPublished: true
}
});
return review;
}
catch (err) {
console.error("[Reviews] Error:", err);
set.status = 500;
return { error: "Failed to create review" };
}
}, {
body: t.Object({
rating: t.Number(),
comment: t.Optional(t.String()),
productId: t.Number(),
customerId: t.Optional(t.Nullable(t.Number())),
customerName: t.Optional(t.String())
})
});
//# sourceMappingURL=reviews.js.map
+6
View File
@@ -24,6 +24,12 @@ export declare const settingsRoute: Elysia<"/settings", {
settings: {
put: any;
};
} & {
settings: {
translations: {
patch: any;
};
};
}, {
derive: {};
resolve: {};
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["settings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAInC,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAmDtB,CAAC"}
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["settings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAInC,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6GtB,CAAC"}
+117
View File
@@ -0,0 +1,117 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
import { adminOnly } from "../middleware/auth";
export const settingsRoute = new Elysia({ prefix: "/settings" })
.get("/", async ({ set }) => {
try {
let settings = await prisma.storeSettings.findFirst();
if (!settings) {
settings = await prisma.storeSettings.create({
data: {}
});
}
return settings;
}
catch (err) {
console.error("[Settings] Error:", err);
set.status = 500;
return { error: "Failed to fetch settings" };
}
})
.use(adminOnly)
.put("/", async ({ body, set }) => {
try {
const data = body;
let settings = await prisma.storeSettings.findFirst();
if (!settings) {
settings = await prisma.storeSettings.create({ data });
}
else {
settings = await prisma.storeSettings.update({
where: { id: settings.id },
data
});
}
return settings;
}
catch (err) {
console.error("[Settings] Error:", err);
set.status = 500;
return { error: "Failed to update settings" };
}
}, {
body: t.Object({
shippingFee: t.Number(),
bankName: t.String(),
bankAccountName: t.String(),
bankAccountNumber: t.String(),
pointsEarnRate: t.Number(),
facebookUrl: t.Optional(t.String()),
instagramUrl: t.Optional(t.String()),
tiktokUrl: t.Optional(t.String()),
footerDescription: t.Optional(t.String()),
newsletterTitle: t.Optional(t.String()),
newsletterDescription: t.Optional(t.String()),
newsletterPlaceholder: t.Optional(t.String()),
newsletterButtonText: t.Optional(t.String()),
footerSupportHeading: t.Optional(t.Any()),
footerCopyrightText: t.Optional(t.Any())
})
})
.patch("/translations", async ({ body, set }) => {
try {
let settings = await prisma.storeSettings.findFirst();
if (!settings) {
settings = await prisma.storeSettings.create({ data: {} });
}
const payload = body;
const results = [];
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
for (const lang of LANGS) {
const footerDescription = typeof payload.footerDescription === 'string' ? (lang === 'th' ? payload.footerDescription : null) : payload.footerDescription?.[lang];
const newsletterTitle = typeof payload.newsletterTitle === 'string' ? (lang === 'th' ? payload.newsletterTitle : null) : payload.newsletterTitle?.[lang];
const newsletterDescription = typeof payload.newsletterDescription === 'string' ? (lang === 'th' ? payload.newsletterDescription : null) : payload.newsletterDescription?.[lang];
const newsletterPlaceholder = typeof payload.newsletterPlaceholder === 'string' ? (lang === 'th' ? payload.newsletterPlaceholder : null) : payload.newsletterPlaceholder?.[lang];
const newsletterButtonText = typeof payload.newsletterButtonText === 'string' ? (lang === 'th' ? payload.newsletterButtonText : null) : payload.newsletterButtonText?.[lang];
if (!footerDescription && !newsletterTitle && !newsletterDescription && !newsletterPlaceholder && !newsletterButtonText)
continue;
const existing = await prisma.storeSettingsTranslation.findUnique({
where: { storeSettingsId_langCode: { storeSettingsId: settings.id, langCode: lang } }
});
const updateData = {};
if (footerDescription)
updateData.footerDescription = footerDescription;
if (newsletterTitle)
updateData.newsletterTitle = newsletterTitle;
if (newsletterDescription)
updateData.newsletterDescription = newsletterDescription;
if (newsletterPlaceholder)
updateData.newsletterPlaceholder = newsletterPlaceholder;
if (newsletterButtonText)
updateData.newsletterButtonText = newsletterButtonText;
if (existing) {
const updated = await prisma.storeSettingsTranslation.update({
where: { id: existing.id },
data: updateData
});
results.push(updated);
}
else {
const created = await prisma.storeSettingsTranslation.create({
data: { storeSettingsId: settings.id, langCode: lang, ...updateData }
});
results.push(created);
}
}
const updatedSettings = await prisma.storeSettings.findFirst({
include: { translations: true }
});
return { success: true, translations: results, settings: updatedSettings };
}
catch (err) {
console.error("[Settings] Translation Error:", err);
set.status = 500;
return { error: "Failed to update settings translation" };
}
});
//# sourceMappingURL=settings.js.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"settings.js","sourceRoot":"","sources":["settings.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"settings.js","sourceRoot":"","sources":["settings.ts"],"names":[],"mappings":"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 truncated
+331
View File
@@ -0,0 +1,331 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
const toNum = (val) => {
if (val === null || val === undefined || val === "")
return 0;
const s = String(val).replace(/[^0-9.\-]/g, "");
const n = parseFloat(s);
return isNaN(n) ? 0 : n;
};
export const shopeeRoute = new Elysia({ prefix: "/shopee" })
// ── Import Orders ─────────────────────────────────────────────────────────
.post("/import/orders", async ({ body, set }) => {
try {
const { rows, shop } = body;
const shopName = shop || "hom-aroma";
console.log(`[Shopee] Received ${rows.length} raw rows for shop: ${shopName}`);
const orderMap = new Map();
for (const row of rows) {
const orderId = String(row["หมายเลขคำสั่งซื้อ"] || "").trim();
if (!orderId)
continue;
if (!orderMap.has(orderId))
orderMap.set(orderId, { meta: row, items: [] });
orderMap.get(orderId).items.push(row);
}
console.log(`[Shopee] Grouped into ${orderMap.size} unique orders`);
const CHUNK_SIZE = 30;
const entries = Array.from(orderMap.entries());
let upserted = 0;
for (let i = 0; i < entries.length; i += CHUNK_SIZE) {
const chunk = entries.slice(i, i + CHUNK_SIZE);
await prisma.$transaction(async (tx) => {
for (const [orderId, { meta, items }] of chunk) {
await tx.shopeeOrderItem.deleteMany({ where: { orderId } });
const itemsData = items.map((item) => ({
parentSku: String(item["เลขอ้างอิง Parent SKU"] || ""),
productName: String(item["ชื่อสินค้า"] || ""),
skuRef: String(item["เลขอ้างอิง SKU (SKU Reference No.)"] || ""),
variantName: String(item["ชื่อตัวเลือก"] || ""),
originalPrice: toNum(item["ราคาตั้งต้น"]),
dealPrice: toNum(item["ราคาขาย"]),
quantity: Math.max(1, Math.round(toNum(item["จำนวน"]))),
totalPayment: toNum(item["ยอดชำระเงิน"]),
rawData: item,
}));
const orderData = {
shop: shopName,
status: String(meta["สถานะการสั่งซื้อ"] || ""),
buyerUsername: String(meta["ชื่อผู้ใช้ (ผู้ซื้อ)"] || ""),
orderCreatedAt: String(meta["วันที่ทำการสั่งซื้อ"] || ""),
paymentTime: String(meta["เวลาการชำระสินค้า"] || ""),
paymentMethod: String(meta["ช่องทางการชำระเงิน"] || ""),
trackingNo: String(meta["*หมายเลขติดตามพัสดุ"] || ""),
totalAmount: toNum(meta["จำนวนเงินทั้งหมด"]),
buyerPaidAmount: toNum(meta["ราคาสินค้าที่ชำระโดยผู้ซื้อ (THB)"]),
shippingFee: toNum(meta["ค่าจัดส่งที่ชำระโดยผู้ซื้อ"]),
commission: toNum(meta["ค่าคอมมิชชั่น"]),
completedAt: String(meta["เวลาที่ทำการสั่งซื้อสำเร็จ"] || ""),
};
await tx.shopeeOrder.upsert({
where: { orderId },
update: { ...orderData, items: { create: itemsData } },
create: { orderId, ...orderData, items: { create: itemsData } },
});
upserted++;
}
}, { timeout: 30000 });
console.log(`[Shopee] Progress: ${Math.min(i + CHUNK_SIZE, entries.length)}/${entries.length}`);
}
console.log(`[Shopee] ✅ Done — ${upserted} orders upserted`);
return { success: true, message: `นำเข้าสำเร็จ ${upserted} คำสั่งซื้อ (จาก ${rows.length} แถว)`, ordersCount: upserted, rowsCount: rows.length };
}
catch (error) {
console.error(`[Shopee] ❌ Error:`, error.message);
set.status = 500;
return { success: false, message: error.message };
}
}, { body: t.Object({ rows: t.Array(t.Any()), shop: t.Optional(t.String()) }) })
// ── GET Single Order ──────────────────────────────────────────────────────
.get("/orders/:orderId", async ({ params, set }) => {
const { orderId } = params;
const order = await prisma.shopeeOrder.findUnique({ where: { orderId }, include: { items: true } });
if (!order) {
set.status = 404;
return { error: "Order not found" };
}
return order;
})
// ── GET Orders (paginated) ────────────────────────────────────────────────
.get("/orders", async ({ query }) => {
const page = Number(query.page ?? 1);
const limit = Number(query.limit ?? 30);
const skip = (page - 1) * limit;
const shop = query.shop || "hom-aroma";
const where = { shop };
const [items, total] = await Promise.all([
prisma.shopeeOrder.findMany({ where, include: { items: true }, orderBy: { orderCreatedAt: "desc" }, skip, take: limit }),
prisma.shopeeOrder.count({ where }),
]);
return { items, total, page, limit, totalPages: Math.ceil(total / limit) };
})
// ── GET Analytics (with date range filter) ───────────────────────────────
.get("/analytics", async ({ query }) => {
const shop = query.shop || "hom-aroma";
const dateFrom = query.dateFrom; // "YYYY-MM-DD"
const dateTo = query.dateTo;
// Build where — orderCreatedAt stored as "YYYY-MM-DD HH:mm" string
const where = { shop };
if (dateFrom || dateTo) {
where.orderCreatedAt = {};
if (dateFrom)
where.orderCreatedAt.gte = dateFrom;
if (dateTo)
where.orderCreatedAt.lte = dateTo + " 23:59";
}
const allOrders = await prisma.shopeeOrder.findMany({
where,
select: {
orderId: true, status: true, orderCreatedAt: true,
totalAmount: true, buyerPaidAmount: true, commission: true,
shippingFee: true, paymentMethod: true,
items: { select: { quantity: true, totalPayment: true } },
},
});
// Monthly aggregation
const monthMap = new Map();
for (const o of allOrders) {
const month = (o.orderCreatedAt ?? "").slice(0, 7) || "Unknown";
if (!monthMap.has(month))
monthMap.set(month, { month, revenue: 0, orders: 0, commission: 0, cancelled: 0, items: 0 });
const m = monthMap.get(month);
m.orders += 1;
m.revenue += o.totalAmount;
m.commission += o.commission;
if (o.status.includes("ยกเลิก"))
m.cancelled += 1;
m.items += o.items.reduce((s, i) => s + i.quantity, 0);
}
const monthly = Array.from(monthMap.values()).sort((a, b) => a.month.localeCompare(b.month));
// Status breakdown
const statusMap = new Map();
for (const o of allOrders) {
let status = o.status;
if (status.includes("สำเร็จแล้ว"))
status = "สำเร็จแล้ว";
else if (status.includes("ได้รับสินค้าแล้ว"))
status = "ได้รับสินค้าแล้ว";
else if (status.includes("ยกเลิก"))
status = "ยกเลิก";
statusMap.set(status, (statusMap.get(status) ?? 0) + 1);
}
const statusBreakdown = Array.from(statusMap.entries())
.map(([status, count]) => ({ status, count }))
.sort((a, b) => b.count - a.count);
// Payment method breakdown
const paymentMap = new Map();
for (const o of allOrders) {
const key = o.paymentMethod || "Unknown";
if (!paymentMap.has(key))
paymentMap.set(key, { count: 0, revenue: 0 });
const p = paymentMap.get(key);
p.count += 1;
p.revenue += o.buyerPaidAmount;
}
const paymentBreakdown = Array.from(paymentMap.entries())
.map(([method, v]) => ({ method, ...v }))
.sort((a, b) => b.count - a.count);
// Top products (from filtered order IDs)
const filteredOrderIds = allOrders.map(o => o.orderId);
const allItems = filteredOrderIds.length > 0
? await prisma.shopeeOrderItem.findMany({
where: { orderId: { in: filteredOrderIds } },
select: { productName: true, variantName: true, quantity: true, totalPayment: true, dealPrice: true },
})
: [];
const productMap = new Map();
for (const item of allItems) {
const key = `${item.productName}__${item.variantName ?? ""}`;
if (!productMap.has(key))
productMap.set(key, { name: item.productName, variant: item.variantName ?? "", qty: 0, revenue: 0, orders: 0 });
const p = productMap.get(key);
p.qty += item.quantity;
p.revenue += item.totalPayment;
p.orders += 1;
}
const topProducts = Array.from(productMap.values()).sort((a, b) => b.revenue - a.revenue).slice(0, 10);
// Summary
const totalRevenue = allOrders.reduce((s, o) => s + o.totalAmount, 0);
const totalCommission = allOrders.reduce((s, o) => s + o.commission, 0);
const totalShipping = allOrders.reduce((s, o) => s + o.shippingFee, 0);
const totalOrders = allOrders.length;
const completedOrders = allOrders.filter(o => !o.status.includes("ยกเลิก")).length;
const cancelledOrders = allOrders.filter(o => o.status.includes("ยกเลิก")).length;
const cancelRate = totalOrders > 0 ? (cancelledOrders / totalOrders) * 100 : 0;
const netRevenue = totalRevenue - totalCommission - totalShipping;
const aov = completedOrders > 0 ? totalRevenue / completedOrders : 0;
return {
monthly, statusBreakdown, paymentBreakdown, topProducts,
summary: {
totalRevenue, totalCommission, totalShipping, netRevenue,
totalOrders, completedOrders, cancelledOrders, cancelRate, aov,
commissionRate: totalRevenue > 0 ? (totalCommission / totalRevenue) * 100 : 0,
},
};
})
// ── GET Analytics Overview (ALL shops combined) ──────────────────────────
.get("/analytics-overview", async ({ query }) => {
const dateFrom = query.dateFrom;
const dateTo = query.dateTo;
const where = {};
if (dateFrom || dateTo) {
where.orderCreatedAt = {};
if (dateFrom)
where.orderCreatedAt.gte = dateFrom;
if (dateTo)
where.orderCreatedAt.lte = dateTo + " 23:59";
}
const allOrders = await prisma.shopeeOrder.findMany({
where,
select: {
orderId: true, shop: true, status: true, orderCreatedAt: true,
totalAmount: true, buyerPaidAmount: true, commission: true,
shippingFee: true,
items: { select: { quantity: true, totalPayment: true } },
},
});
// Per-shop summary
const shopMap = new Map();
for (const o of allOrders) {
if (!shopMap.has(o.shop)) {
shopMap.set(o.shop, { shop: o.shop, orders: 0, completed: 0, cancelled: 0, revenue: 0, commission: 0, shippingFee: 0, items: 0 });
}
const s = shopMap.get(o.shop);
s.orders += 1;
s.revenue += o.totalAmount;
s.commission += o.commission;
s.shippingFee += o.shippingFee;
s.items += o.items.reduce((acc, i) => acc + i.quantity, 0);
if (o.status.includes("ยกเลิก"))
s.cancelled += 1;
else
s.completed += 1;
}
const byShop = Array.from(shopMap.values()).map(s => ({
...s,
net: s.revenue - s.commission - s.shippingFee,
cancelRate: s.orders > 0 ? (s.cancelled / s.orders) * 100 : 0,
aov: s.completed > 0 ? s.revenue / s.completed : 0,
})).sort((a, b) => b.revenue - a.revenue);
// Monthly stacked by shop
const monthShopMap = new Map();
for (const o of allOrders) {
const month = (o.orderCreatedAt ?? "").slice(0, 7) || "Unknown";
if (!monthShopMap.has(month))
monthShopMap.set(month, new Map());
const sm = monthShopMap.get(month);
if (!sm.has(o.shop))
sm.set(o.shop, { revenue: 0, orders: 0, commission: 0 });
const ms = sm.get(o.shop);
ms.revenue += o.totalAmount;
ms.orders += 1;
ms.commission += o.commission;
}
const shops = Array.from(shopMap.keys());
const monthly = Array.from(monthShopMap.entries())
.map(([month, sm]) => {
const row = { month };
for (const shop of shops) {
const d = sm.get(shop) ?? { revenue: 0, orders: 0, commission: 0 };
row[`${shop}_revenue`] = d.revenue;
row[`${shop}_orders`] = d.orders;
}
row.totalRevenue = Array.from(sm.values()).reduce((s, v) => s + v.revenue, 0);
row.totalOrders = Array.from(sm.values()).reduce((s, v) => s + v.orders, 0);
return row;
})
.sort((a, b) => a.month.localeCompare(b.month));
// Top products across all shops (filtered by date range via orderId)
const filteredOrderIds = allOrders.map(o => o.orderId);
const allItems = filteredOrderIds.length > 0 ? await prisma.shopeeOrderItem.findMany({
where: { orderId: { in: filteredOrderIds } },
select: { productName: true, variantName: true, quantity: true, totalPayment: true, order: { select: { shop: true } } },
}) : [];
const productMap = new Map();
for (const item of allItems) {
const key = `${item.productName}__${item.variantName ?? ""}`;
if (!productMap.has(key))
productMap.set(key, { name: item.productName, variant: item.variantName ?? "", qty: 0, revenue: 0 });
const p = productMap.get(key);
p.qty += item.quantity;
p.revenue += item.totalPayment;
}
const topProducts = Array.from(productMap.values()).sort((a, b) => b.revenue - a.revenue).slice(0, 10);
// Grand totals
const totalRevenue = allOrders.reduce((s, o) => s + o.totalAmount, 0);
const totalCommission = allOrders.reduce((s, o) => s + o.commission, 0);
const totalShipping = allOrders.reduce((s, o) => s + o.shippingFee, 0);
const totalOrders = allOrders.length;
const totalCancelled = allOrders.filter(o => o.status.includes("ยกเลิก")).length;
return {
byShop, monthly, topProducts, shops,
totals: {
totalRevenue, totalCommission, totalShipping,
netRevenue: totalRevenue - totalCommission - totalShipping,
totalOrders, totalCancelled,
cancelRate: totalOrders > 0 ? (totalCancelled / totalOrders) * 100 : 0,
aov: (totalOrders - totalCancelled) > 0 ? totalRevenue / (totalOrders - totalCancelled) : 0,
commissionRate: totalRevenue > 0 ? (totalCommission / totalRevenue) * 100 : 0,
},
};
})
// ── GET Summary ───────────────────────────────────────────────────────────
.get("/summary", async ({ query }) => {
const shop = query.shop || "hom-aroma";
const where = { shop };
const [orderCount, agg, itemAgg] = await Promise.all([
prisma.shopeeOrder.count({ where }),
prisma.shopeeOrder.aggregate({ where, _sum: { totalAmount: true, buyerPaidAmount: true, commission: true, shippingFee: true } }),
prisma.shopeeOrderItem.aggregate({ where: { order: where }, _sum: { quantity: true, totalPayment: true } }),
]);
return {
orderCount,
totalAmount: agg._sum.totalAmount ?? 0,
buyerPaidAmount: agg._sum.buyerPaidAmount ?? 0,
totalCommission: agg._sum.commission ?? 0,
totalShipping: agg._sum.shippingFee ?? 0,
totalItems: itemAgg._sum.quantity ?? 0,
totalItemRevenue: itemAgg._sum.totalPayment ?? 0,
};
});
//# sourceMappingURL=shopee.js.map
+261
View File
@@ -0,0 +1,261 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const tiktokRoute = new Elysia({ prefix: "/tiktok" })
.post("/import/income", async ({ body, set }) => {
try {
const { data } = body;
// ── Debug logging ────────────────────────────────────────────────────
console.log(`[TikTok/Income] Received ${data.length} rows`);
if (data.length > 0) {
console.log(`[TikTok/Income] First row keys:`, Object.keys(data[0]));
console.log(`[TikTok/Income] First row id:`, data[0].id);
console.log(`[TikTok/Income] First row sample:`, JSON.stringify(data[0]).slice(0, 300));
}
// Check for duplicate/missing IDs
const ids = data.map((r) => r.id);
const uniqueIds = new Set(ids);
console.log(`[TikTok/Income] Total IDs: ${ids.length}, Unique IDs: ${uniqueIds.size}`);
if (uniqueIds.size < ids.length) {
console.warn(`[TikTok/Income] ⚠️ Duplicate IDs detected!`);
}
// ── Chunked upsert (avoid transaction timeout on large files) ────────
const CHUNK_SIZE = 50;
let inserted = 0;
for (let i = 0; i < data.length; i += CHUNK_SIZE) {
const chunk = data.slice(i, i + CHUNK_SIZE);
await prisma.$transaction(chunk.map((row) => prisma.tiktokIncome.upsert({
where: { id: row.id },
update: {
type: row.type,
orderCreatedTime: row.orderCreatedTime,
orderSettledTime: row.orderSettledTime,
currency: row.currency,
totalSettlementAmount: row.totalSettlementAmount,
totalRevenue: row.totalRevenue,
totalFees: row.totalFees,
rawData: row.rawData,
},
create: {
id: row.id,
type: row.type,
orderCreatedTime: row.orderCreatedTime,
orderSettledTime: row.orderSettledTime,
currency: row.currency,
totalSettlementAmount: row.totalSettlementAmount,
totalRevenue: row.totalRevenue,
totalFees: row.totalFees,
rawData: row.rawData,
},
})), { timeout: 30000 });
inserted += chunk.length;
console.log(`[TikTok/Income] Progress: ${inserted}/${data.length}`);
}
console.log(`[TikTok/Income] ✅ Done — ${inserted} records upserted`);
return { success: true, message: `Imported ${inserted} income records.` };
}
catch (error) {
console.error(`[TikTok/Income] ❌ Error:`, error.message);
set.status = 500;
return { success: false, message: error.message };
}
}, {
body: t.Object({
data: t.Array(t.Any()),
}),
})
.post("/import/product", async ({ body, set }) => {
try {
const { data } = body;
await prisma.$transaction(data.map((row) => prisma.tiktokProduct.upsert({
where: { id: row.id },
update: {
productName: row.productName,
status: row.status,
gmv: row.gmv,
itemsSold: row.itemsSold,
orders: row.orders,
rawData: row.rawData,
},
create: {
id: row.id,
productName: row.productName,
status: row.status,
gmv: row.gmv,
itemsSold: row.itemsSold,
orders: row.orders,
rawData: row.rawData,
},
})));
return { success: true, message: `Imported ${data.length} product records.` };
}
catch (error) {
set.status = 500;
return { success: false, message: error.message };
}
}, {
body: t.Object({
data: t.Array(t.Any()),
}),
})
.post("/import/product-sku", async ({ body, set }) => {
try {
const { data } = body;
await prisma.$transaction(data.map((row) => prisma.tiktokProductSku.upsert({
where: { id: row.id },
update: {
productId: row.productId,
skuName: row.skuName,
status: row.status,
gmv: row.gmv,
skuOrders: row.skuOrders,
itemsSold: row.itemsSold,
rawData: row.rawData,
},
create: {
id: row.id,
productId: row.productId,
skuName: row.skuName,
status: row.status,
gmv: row.gmv,
skuOrders: row.skuOrders,
itemsSold: row.itemsSold,
rawData: row.rawData,
},
})));
return { success: true, message: `Imported ${data.length} SKU records.` };
}
catch (error) {
set.status = 500;
return { success: false, message: error.message };
}
}, {
body: t.Object({
data: t.Array(t.Any()),
}),
})
// ── GET endpoints ────────────────────────────────────────────────────────────
.get("/income", async ({ query }) => {
const page = Number(query.page ?? 1);
const limit = Number(query.limit ?? 50);
const skip = (page - 1) * limit;
const [items, total] = await Promise.all([
prisma.tiktokIncome.findMany({
orderBy: { orderCreatedTime: "desc" },
skip,
take: limit,
}),
prisma.tiktokIncome.count(),
]);
return { items, total, page, limit, totalPages: Math.ceil(total / limit) };
})
.get("/product", async ({ query }) => {
const page = Number(query.page ?? 1);
const limit = Number(query.limit ?? 50);
const skip = (page - 1) * limit;
const [items, total] = await Promise.all([
prisma.tiktokProduct.findMany({
orderBy: { gmv: "desc" },
skip,
take: limit,
}),
prisma.tiktokProduct.count(),
]);
return { items, total, page, limit, totalPages: Math.ceil(total / limit) };
})
.get("/product-sku", async ({ query }) => {
const page = Number(query.page ?? 1);
const limit = Number(query.limit ?? 50);
const skip = (page - 1) * limit;
const [items, total] = await Promise.all([
prisma.tiktokProductSku.findMany({
orderBy: { gmv: "desc" },
skip,
take: limit,
}),
prisma.tiktokProductSku.count(),
]);
return { items, total, page, limit, totalPages: Math.ceil(total / limit) };
})
.get("/summary", async () => {
const [incomeCount, productCount, skuCount, incomeAgg, productAgg] = await Promise.all([
prisma.tiktokIncome.count(),
prisma.tiktokProduct.count(),
prisma.tiktokProductSku.count(),
prisma.tiktokIncome.aggregate({ _sum: { totalRevenue: true, totalSettlementAmount: true } }),
prisma.tiktokProduct.aggregate({ _sum: { gmv: true, itemsSold: true, orders: true } }),
]);
return {
incomeCount,
productCount,
skuCount,
totalRevenue: incomeAgg._sum.totalRevenue ?? 0,
totalSettlement: incomeAgg._sum.totalSettlementAmount ?? 0,
totalGmv: productAgg._sum.gmv ?? 0,
totalItemsSold: productAgg._sum.itemsSold ?? 0,
totalOrders: productAgg._sum.orders ?? 0,
};
})
.get("/analysis/sku", async () => {
// 1. Top 10 by GMV
const topGmv = await prisma.$queryRaw `
WITH SkuStats AS (
SELECT "id", "productId", "skuName", "gmv", "skuOrders", "itemsSold",
CASE WHEN "itemsSold" > 0 THEN "gmv" / "itemsSold" ELSE 0 END AS "avgPrice",
CASE WHEN "skuOrders" > 0 THEN "gmv" / "skuOrders" ELSE 0 END AS "aov"
FROM "TiktokProductSku"
)
SELECT * FROM SkuStats ORDER BY "gmv" DESC LIMIT 10
`;
// 2. Top 10 by Volume (itemsSold)
const topVolume = await prisma.$queryRaw `
WITH SkuStats AS (
SELECT "id", "productId", "skuName", "gmv", "skuOrders", "itemsSold",
CASE WHEN "itemsSold" > 0 THEN "gmv" / "itemsSold" ELSE 0 END AS "avgPrice",
CASE WHEN "skuOrders" > 0 THEN "gmv" / "skuOrders" ELSE 0 END AS "aov"
FROM "TiktokProductSku"
)
SELECT * FROM SkuStats ORDER BY "itemsSold" DESC LIMIT 10
`;
// 3. Scent/Variant Grouping
const scentAnalysis = await prisma.$queryRaw `
WITH SplitNames AS (
SELECT
"id",
"gmv",
"itemsSold",
"skuOrders",
TRIM(SPLIT_PART("skuName", ':', 2)) AS "scentName"
FROM "TiktokProductSku"
WHERE "skuName" LIKE '%:%'
)
SELECT
"scentName",
SUM("gmv") AS "totalGmv",
SUM("itemsSold") AS "totalItems",
SUM("skuOrders") AS "totalOrders",
COUNT("id") AS "skuCount"
FROM SplitNames
GROUP BY "scentName"
ORDER BY "totalGmv" DESC
LIMIT 15
`;
// 4. Overall Summary (Average AOV, Total GMV)
const summaryList = await prisma.$queryRaw `
SELECT
SUM("gmv") AS "totalGmv",
SUM("itemsSold") AS "totalItems",
SUM("skuOrders") AS "totalOrders",
CASE WHEN SUM("skuOrders") > 0 THEN SUM("gmv") / SUM("skuOrders") ELSE 0 END AS "avgAov",
CASE WHEN SUM("itemsSold") > 0 THEN SUM("gmv") / SUM("itemsSold") ELSE 0 END AS "avgPrice"
FROM "TiktokProductSku"
`;
// Handle BigInt conversion since Prisma $queryRaw returns BigInts for COUNT/SUM in Postgres sometimes.
const serialize = (data) => JSON.parse(JSON.stringify(data, (key, value) => (typeof value === "bigint" ? Number(value) : value)));
return serialize({
summary: summaryList[0] || null,
topGmv,
topVolume,
scentAnalysis
});
});
//# sourceMappingURL=tiktok.js.map
+371
View File
@@ -0,0 +1,371 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
const toNum = (val) => {
if (val === null || val === undefined || val === "")
return 0;
const s = String(val).replace(/[^0-9.\-]/g, "");
const n = parseFloat(s);
return isNaN(n) ? 0 : n;
};
// TikTok CSV values can have trailing \t — trim everything
const str = (val) => String(val ?? "").replace(/\t/g, "").trim();
// TikTok dates are "DD/MM/YYYY HH:mm:ss" -> convert to "YYYY-MM-DD HH:mm:ss" for sorting/filtering
const parseTiktokDate = (val) => {
const d = str(val);
if (!d)
return "";
const [datePart, timePart] = d.split(" ");
if (!datePart)
return d;
const parts = datePart.split("/");
if (parts.length === 3) {
const [day, month, year] = parts;
return `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")} ${timePart || "00:00:00"}`;
}
return d;
};
export const tiktokOrderRoute = new Elysia({ prefix: "/tiktok-order" })
// ── Import (CSV rows as JSON) ──────────────────────────────────────────────
.post("/import", async ({ body, set }) => {
try {
const { rows, shop } = body;
const shopName = shop || "hom-aroma";
console.log(`[TiktokOrder] Received ${rows.length} raw rows for shop: ${shopName}`);
// Group rows by Order ID (trim tabs)
const orderMap = new Map();
for (const row of rows) {
const orderId = str(row["Order ID"]);
if (!orderId)
continue;
if (!orderMap.has(orderId))
orderMap.set(orderId, { meta: row, items: [] });
orderMap.get(orderId).items.push(row);
}
console.log(`[TiktokOrder] Grouped into ${orderMap.size} unique orders`);
const CHUNK = 30;
const entries = Array.from(orderMap.entries());
let upserted = 0;
for (let i = 0; i < entries.length; i += CHUNK) {
const chunk = entries.slice(i, i + CHUNK);
await prisma.$transaction(async (tx) => {
for (const [orderId, { meta, items }] of chunk) {
// Delete old items first (sync strategy)
await tx.tiktokOrderItem.deleteMany({ where: { orderId } });
const itemsData = items.map((item) => ({
skuId: str(item["SKU ID"]),
sellerSku: str(item["Seller SKU"]),
productName: str(item["Product Name"]),
variation: str(item["Variation"]),
quantity: Math.max(1, Math.round(toNum(item["Quantity"]))),
skuQtyReturn: Math.round(toNum(item["Sku Quantity of return"])),
unitOriginalPrice: toNum(item["SKU Unit Original Price"]),
subtotalBeforeDiscount: toNum(item["SKU Subtotal Before Discount"]),
platformDiscount: toNum(item["SKU Platform Discount"]),
sellerDiscount: toNum(item["SKU Seller Discount"]),
subtotalAfterDiscount: toNum(item["SKU Subtotal After Discount"]),
productCategory: str(item["Product Category"]),
weight: toNum(item["Weight(kg)"]),
}));
const orderData = {
shop: shopName,
status: str(meta["Order Status"]),
subStatus: str(meta["Order Substatus"]),
orderType: str(meta["Normal or Pre-order"]),
cancelBy: str(meta["Cancel By"]),
cancelReason: str(meta["Cancel Reason"]),
fulfillmentType: str(meta["Fulfillment Type"]),
warehouseName: str(meta["Warehouse Name"]),
trackingId: str(meta["Tracking ID"]),
deliveryOption: str(meta["Delivery Option"]),
shippingProvider: str(meta["Shipping Provider Name"]),
buyerUsername: str(meta["Buyer Username"]),
paymentMethod: str(meta["Payment Method"]),
orderAmount: toNum(meta["Order Amount"]),
refundAmount: toNum(meta["Order Refund Amount"]),
shippingFee: toNum(meta["Shipping Fee After Discount"]),
originalShipping: toNum(meta["Original Shipping Fee"]),
taxes: toNum(meta["Taxes"]),
province: str(meta["Province"]),
district: str(meta["District"]),
country: str(meta["Country"]),
createdTime: parseTiktokDate(meta["Created Time"]),
paidTime: parseTiktokDate(meta["Paid Time"]),
rtsTime: parseTiktokDate(meta["RTS Time"]),
shippedTime: parseTiktokDate(meta["Shipped Time"]),
deliveredTime: parseTiktokDate(meta["Delivered Time"]),
cancelledTime: parseTiktokDate(meta["Cancelled Time"]),
};
await tx.tiktokOrder.upsert({
where: { orderId },
update: { ...orderData, items: { create: itemsData } },
create: { orderId, ...orderData, items: { create: itemsData } },
});
upserted++;
}
}, { timeout: 30000 });
console.log(`[TiktokOrder] Progress: ${Math.min(i + CHUNK, entries.length)}/${entries.length}`);
}
console.log(`[TiktokOrder] ✅ Done — ${upserted} orders upserted`);
return { success: true, message: `นำเข้าสำเร็จ ${upserted} คำสั่งซื้อ (${rows.length} แถว)`, ordersCount: upserted, rowsCount: rows.length };
}
catch (error) {
console.error(`[TiktokOrder] ❌ Error:`, error.message);
set.status = 500;
return { success: false, message: error.message };
}
}, { body: t.Object({ rows: t.Array(t.Any()), shop: t.Optional(t.String()) }) })
// ── Clear Shop Data ───────────────────────────────────────────────────────
.delete("/clear", async ({ query, set }) => {
try {
const shop = query.shop || "hom-aroma";
const result = await prisma.tiktokOrder.deleteMany({ where: { shop } });
return { success: true, message: `ลบข้อมูลร้าน ${shop} สำเร็จ (${result.count} รายการ)`, count: result.count };
}
catch (error) {
set.status = 500;
return { success: false, message: error.message };
}
})
// ── GET Single Order ──────────────────────────────────────────────────────
.get("/orders/:orderId", async ({ params, set }) => {
const order = await prisma.tiktokOrder.findUnique({ where: { orderId: params.orderId }, include: { items: true } });
if (!order) {
set.status = 404;
return { error: "Order not found" };
}
return order;
})
// ── GET Orders (paginated) ────────────────────────────────────────────────
.get("/orders", async ({ query }) => {
const page = Number(query.page ?? 1);
const limit = Number(query.limit ?? 30);
const skip = (page - 1) * limit;
const shop = query.shop || "hom-aroma";
const where = { shop };
const [items, total] = await Promise.all([
prisma.tiktokOrder.findMany({ where, include: { items: true }, orderBy: { createdTime: "desc" }, skip, take: limit }),
prisma.tiktokOrder.count({ where }),
]);
return { items, total, page, limit, totalPages: Math.ceil(total / limit) };
})
// ── GET Summary ───────────────────────────────────────────────────────────
.get("/summary", async ({ query }) => {
const shop = query.shop || "hom-aroma";
const where = { shop };
const [orderCount, agg, itemAgg] = await Promise.all([
prisma.tiktokOrder.count({ where }),
prisma.tiktokOrder.aggregate({ where, _sum: { orderAmount: true, shippingFee: true, refundAmount: true, taxes: true } }),
prisma.tiktokOrderItem.aggregate({ where: { order: where }, _sum: { quantity: true, subtotalAfterDiscount: true } }),
]);
return {
orderCount,
totalAmount: agg._sum.orderAmount ?? 0,
totalShipping: agg._sum.shippingFee ?? 0,
totalRefund: agg._sum.refundAmount ?? 0,
totalTaxes: agg._sum.taxes ?? 0,
totalItems: itemAgg._sum.quantity ?? 0,
totalSkuRevenue: itemAgg._sum.subtotalAfterDiscount ?? 0,
};
})
// ── GET Analytics (date range filter) ────────────────────────────────────
.get("/analytics", async ({ query }) => {
const shop = query.shop || "hom-aroma";
const dateFrom = query.dateFrom;
const dateTo = query.dateTo;
const where = { shop };
if (dateFrom || dateTo) {
where.createdTime = {};
if (dateFrom)
where.createdTime.gte = dateFrom;
if (dateTo)
where.createdTime.lte = dateTo + " 23:59";
}
const allOrders = await prisma.tiktokOrder.findMany({
where,
select: {
orderId: true, status: true, subStatus: true, createdTime: true,
orderAmount: true, shippingFee: true, refundAmount: true, taxes: true,
paymentMethod: true, province: true,
items: { select: { quantity: true, subtotalAfterDiscount: true, platformDiscount: true, sellerDiscount: true } },
},
});
// Monthly aggregation
const monthMap = new Map();
for (const o of allOrders) {
const month = (o.createdTime ?? "").slice(0, 7) || "Unknown";
if (!monthMap.has(month))
monthMap.set(month, { month, revenue: 0, orders: 0, cancelled: 0, items: 0, platformDiscount: 0, sellerDiscount: 0 });
const m = monthMap.get(month);
m.orders += 1;
m.revenue += o.orderAmount;
m.platformDiscount += o.items.reduce((s, i) => s + i.platformDiscount, 0);
m.sellerDiscount += o.items.reduce((s, i) => s + i.sellerDiscount, 0);
if (o.status.includes("ยกเลิก") || o.status.includes("cancel"))
m.cancelled += 1;
m.items += o.items.reduce((s, i) => s + i.quantity, 0);
}
const monthly = Array.from(monthMap.values()).sort((a, b) => a.month.localeCompare(b.month));
// Status breakdown
const statusMap = new Map();
for (const o of allOrders) {
const key = o.status || "Unknown";
statusMap.set(key, (statusMap.get(key) ?? 0) + 1);
}
const statusBreakdown = Array.from(statusMap.entries()).map(([status, count]) => ({ status, count })).sort((a, b) => b.count - a.count);
// Payment method breakdown
const payMap = new Map();
for (const o of allOrders) {
const key = o.paymentMethod || "Unknown";
payMap.set(key, (payMap.get(key) ?? 0) + 1);
}
const paymentBreakdown = Array.from(payMap.entries()).map(([method, count]) => ({ method, count })).sort((a, b) => b.count - a.count);
// Top products
const filteredIds = allOrders.map(o => o.orderId);
const allItems = filteredIds.length > 0
? await prisma.tiktokOrderItem.findMany({
where: { orderId: { in: filteredIds } },
select: { productName: true, variation: true, quantity: true, subtotalAfterDiscount: true, sellerSku: true },
})
: [];
const productMap = new Map();
for (const item of allItems) {
const key = `${item.productName}__${item.variation ?? ""}`;
if (!productMap.has(key))
productMap.set(key, { name: item.productName, sku: item.sellerSku ?? "", variation: item.variation ?? "", qty: 0, revenue: 0 });
const p = productMap.get(key);
p.qty += item.quantity;
p.revenue += item.subtotalAfterDiscount;
}
const topProducts = Array.from(productMap.values()).sort((a, b) => b.revenue - a.revenue).slice(0, 10);
// Summary
const totalRevenue = allOrders.reduce((s, o) => s + o.orderAmount, 0);
const totalShipping = allOrders.reduce((s, o) => s + o.shippingFee, 0);
const totalRefund = allOrders.reduce((s, o) => s + o.refundAmount, 0);
const totalPlatformDisc = allOrders.flatMap(o => o.items).reduce((s, i) => s + i.platformDiscount, 0);
const totalSellerDisc = allOrders.flatMap(o => o.items).reduce((s, i) => s + i.sellerDiscount, 0);
const totalOrders = allOrders.length;
const cancelledOrders = allOrders.filter(o => o.status.includes("ยกเลิก")).length;
const cancelRate = totalOrders > 0 ? (cancelledOrders / totalOrders) * 100 : 0;
const completedOrders = totalOrders - cancelledOrders;
const aov = completedOrders > 0 ? totalRevenue / completedOrders : 0;
return {
monthly, statusBreakdown, paymentBreakdown, topProducts,
summary: {
totalRevenue, totalShipping, totalRefund, totalPlatformDisc, totalSellerDisc,
netRevenue: totalRevenue - totalShipping,
totalOrders, completedOrders, cancelledOrders, cancelRate, aov,
},
};
})
// ── GET Analytics Overview (ALL shops combined) ──────────────────────────
.get("/analytics-overview", async ({ query }) => {
const dateFrom = query.dateFrom;
const dateTo = query.dateTo;
const where = {};
if (dateFrom || dateTo) {
where.createdTime = {};
if (dateFrom)
where.createdTime.gte = dateFrom;
if (dateTo)
where.createdTime.lte = dateTo + " 23:59";
}
const allOrders = await prisma.tiktokOrder.findMany({
where,
select: {
orderId: true, shop: true, status: true, createdTime: true,
orderAmount: true, shippingFee: true, refundAmount: true,
items: { select: { quantity: true, platformDiscount: true, sellerDiscount: true, subtotalAfterDiscount: true } },
},
});
// Per-shop summary
const shopMap = new Map();
for (const o of allOrders) {
if (!shopMap.has(o.shop)) {
shopMap.set(o.shop, { shop: o.shop, orders: 0, completed: 0, cancelled: 0, revenue: 0, shippingFee: 0, platformDiscount: 0, sellerDiscount: 0, items: 0 });
}
const s = shopMap.get(o.shop);
s.orders += 1;
s.revenue += o.orderAmount;
s.shippingFee += o.shippingFee;
s.platformDiscount += o.items.reduce((acc, i) => acc + i.platformDiscount, 0);
s.sellerDiscount += o.items.reduce((acc, i) => acc + i.sellerDiscount, 0);
s.items += o.items.reduce((acc, i) => acc + i.quantity, 0);
if (o.status.includes("ยกเลิก") || o.status.includes("cancel"))
s.cancelled += 1;
else
s.completed += 1;
}
const shops = Array.from(shopMap.values());
// Monthly stacked by shop
const monthShopMap = new Map();
for (const o of allOrders) {
const month = (o.createdTime ?? "").slice(0, 7) || "Unknown";
if (!monthShopMap.has(month))
monthShopMap.set(month, new Map());
const sm = monthShopMap.get(month);
if (!sm.has(o.shop))
sm.set(o.shop, { revenue: 0, orders: 0 });
const ms = sm.get(o.shop);
ms.revenue += o.orderAmount;
ms.orders += 1;
}
const shopKeys = Array.from(shopMap.keys());
const monthly = Array.from(monthShopMap.entries())
.map(([month, sm]) => {
const row = { month };
for (const shop of shopKeys) {
const d = sm.get(shop) ?? { revenue: 0, orders: 0 };
row[`${shop}_revenue`] = d.revenue;
row[`${shop}_orders`] = d.orders;
}
row.totalRevenue = Array.from(sm.values()).reduce((s, v) => s + v.revenue, 0);
row.totalOrders = Array.from(sm.values()).reduce((s, v) => s + v.orders, 0);
row.revenue = row.totalRevenue; // Fallback for global view
return row;
})
.sort((a, b) => a.month.localeCompare(b.month));
// Top Products Global
const filteredIds = allOrders.map(o => o.orderId);
const allItems = filteredIds.length > 0
? await prisma.tiktokOrderItem.findMany({
where: { orderId: { in: filteredIds } },
select: { productName: true, variation: true, quantity: true, subtotalAfterDiscount: true },
})
: [];
const productMap = new Map();
for (const item of allItems) {
const key = `${item.productName}__${item.variation ?? ""}`;
if (!productMap.has(key))
productMap.set(key, { name: item.productName, variation: item.variation ?? "", qty: 0, revenue: 0 });
const p = productMap.get(key);
p.qty += item.quantity;
p.revenue += item.subtotalAfterDiscount;
}
const topProducts = Array.from(productMap.values()).sort((a, b) => b.revenue - a.revenue).slice(0, 10);
// Global summary
const totalRevenue = shops.reduce((s, sh) => s + sh.revenue, 0);
const totalShipping = shops.reduce((s, sh) => s + sh.shippingFee, 0);
const totalPlatformDisc = shops.reduce((s, sh) => s + sh.platformDiscount, 0);
const totalSellerDisc = shops.reduce((s, sh) => s + sh.sellerDiscount, 0);
const totalOrders = shops.reduce((s, sh) => s + sh.orders, 0);
const completedOrders = shops.reduce((s, sh) => s + sh.completed, 0);
const cancelledOrders = shops.reduce((s, sh) => s + sh.cancelled, 0);
return {
shops,
monthly,
topProducts,
summary: {
totalRevenue,
totalShipping,
totalPlatformDisc,
totalSellerDisc,
netRevenue: totalRevenue - totalShipping,
totalOrders,
completedOrders,
cancelledOrders,
cancelRate: totalOrders > 0 ? (cancelledOrders / totalOrders) * 100 : 0,
aov: completedOrders > 0 ? totalRevenue / completedOrders : 0,
}
};
});
//# sourceMappingURL=tiktokOrder.js.map
+2
View File
@@ -29,10 +29,12 @@ export declare const uploadRoute: Elysia<"/upload", {
success?: never;
url?: never;
filename?: never;
path?: never;
} | {
success: boolean;
url: string;
filename: string;
path: string;
error?: never;
};
422: {
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["upload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAInC,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwDpB,CAAC"}
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["upload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAMnC,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsEpB,CAAC"}
+67
View File
@@ -0,0 +1,67 @@
import { Elysia, t } from "elysia";
import { join } from "path";
import { mkdir } from "fs/promises";
import { randomUUID } from "crypto";
import prisma from "../lib/prisma";
export const uploadRoute = new Elysia({ prefix: "/upload" })
.post("/", async ({ body, set }) => {
const { file } = body;
if (!file) {
set.status = 400;
return { error: "No file uploaded" };
}
// Validate file type
if (!file.type.startsWith("image/")) {
set.status = 400;
return { error: "Only image files are allowed" };
}
console.log(`[Upload] Processing: ${file.name} (${(file.size / 1024).toFixed(2)} KB, ${file.type})`);
// Generate unique filename
const ext = file.name.split(".").pop();
const filename = `${randomUUID()}.${ext}`;
const uploadDir = join(process.cwd(), "public", "uploads");
const filePath = join(uploadDir, filename);
try {
// Ensure directory exists
await mkdir(uploadDir, { recursive: true });
// Write file using Bun.write
await Bun.write(filePath, await file.arrayBuffer());
const relativePath = `/uploads/${filename}`;
const mimeType = file.type;
const sizeBytes = file.size;
await prisma.media.create({
data: {
filename,
path: relativePath,
mimeType,
sizeBytes,
}
});
const BACKEND_PUBLIC_URL = process.env.BACKEND_PUBLIC_URL || "http://localhost:3051";
const publicUrl = `${BACKEND_PUBLIC_URL}${relativePath}`;
console.log(`[Upload] Success: ${filename} -> ${publicUrl}`);
return {
success: true,
url: publicUrl,
filename,
path: relativePath
};
}
catch (err) {
console.error("[Upload] Error:", err);
set.status = 500;
return { error: "Failed to save file" };
}
}, {
body: t.Object({
file: t.File({
maxSize: '5m',
type: ['image/jpeg', 'image/png', 'image/webp', 'image/gif']
})
}),
detail: {
summary: "Upload product image",
tags: ["upload"]
}
});
//# sourceMappingURL=upload.js.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"upload.js","sourceRoot":"","sources":["upload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,MAAM,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;KACzD,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE;IACjC,MAAM,EAAE,IAAI,EAAE,GAAG,IAAsB,CAAC;IAExC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC;IACvC,CAAC;IAED,qBAAqB;IACrB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpC,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,8BAA8B,EAAE,CAAC;IACnD,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,wBAAwB,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;IAErG,2BAA2B;IAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;IACvC,MAAM,QAAQ,GAAG,GAAG,MAAM,CAAC,UAAU,EAAE,IAAI,GAAG,EAAE,CAAC;IACjD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;IAC3D,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAE3C,IAAI,CAAC;QACH,0BAA0B;QAC1B,MAAM,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAE5C,6BAA6B;QAC7B,MAAM,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;QAEpD,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,uBAAuB,CAAC;QAC/D,MAAM,SAAS,GAAG,GAAG,OAAO,YAAY,QAAQ,EAAE,CAAC;QAEnD,OAAO,CAAC,GAAG,CAAC,qBAAqB,QAAQ,OAAO,SAAS,EAAE,CAAC,CAAC;QAE7D,OAAO;YACL,OAAO,EAAE,IAAI;YACb,GAAG,EAAE,SAAS;YACd,QAAQ;SACT,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC;QACtC,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC;IAC1C,CAAC;AACH,CAAC,EAAE;IACD,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QACb,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;YACX,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,CAAC,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,CAAC;SAC7D,CAAC;KACH,CAAC;IACF,MAAM,EAAE;QACN,OAAO,EAAE,sBAAsB;QAC/B,IAAI,EAAE,CAAC,QAAQ,CAAC;KACjB;CACF,CAAC,CAAC"}
{"version":3,"file":"upload.js","sourceRoot":"","sources":["upload.ts"],"names":[],"mappings":"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"}
+2 -1
View File
@@ -1,6 +1,7 @@
import { Elysia, t } from "elysia";
import { join } from "path";
import { mkdir } from "fs/promises";
import { randomUUID } from "crypto";
import prisma from "../lib/prisma";
export const uploadRoute = new Elysia({ prefix: "/upload" })
@@ -22,7 +23,7 @@ export const uploadRoute = new Elysia({ prefix: "/upload" })
// Generate unique filename
const ext = file.name.split(".").pop();
const filename = `${crypto.randomUUID()}.${ext}`;
const filename = `${randomUUID()}.${ext}`;
const uploadDir = join(process.cwd(), "public", "uploads");
const filePath = join(uploadDir, filename);