x
This commit is contained in:
1 parent
a128b9c163
commit
7a352d4998
77 files changed
+5481
-21
No files matched your search
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export {};
|
||||||
|
//# sourceMappingURL=check_cat.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"check_cat.d.ts","sourceRoot":"","sources":["check_cat.ts"],"names":[],"mappings":""}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
async function main() {
|
||||||
|
const cats = await prisma.category.findMany({
|
||||||
|
include: { translations: true }
|
||||||
|
});
|
||||||
|
console.log(JSON.stringify(cats, null, 2));
|
||||||
|
}
|
||||||
|
main();
|
||||||
|
//# sourceMappingURL=check_cat.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"check_cat.js","sourceRoot":"","sources":["check_cat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,MAAM,MAAM,GAAG,IAAI,YAAY,EAAE,CAAA;AACjC,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAC1C,OAAO,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE;KAChC,CAAC,CAAA;IACF,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;AAC5C,CAAC;AACD,IAAI,EAAE,CAAA"}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun --watch src/index.ts",
|
"dev": "bun --watch src/index.ts",
|
||||||
|
"build": "prisma generate",
|
||||||
"start": "bun src/index.ts"
|
"start": "bun src/index.ts"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 592 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 592 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 592 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 592 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 592 KiB |
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export {};
|
||||||
|
//# sourceMappingURL=migrate-pages.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"migrate-pages.d.ts","sourceRoot":"","sources":["migrate-pages.ts"],"names":[],"mappings":""}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import prisma from '../src/lib/prisma';
|
||||||
|
async function main() {
|
||||||
|
console.log('Migrating pages to CMS...');
|
||||||
|
// Delete existing to force fresh payload insert
|
||||||
|
await prisma.page.deleteMany({
|
||||||
|
where: { slug: { in: ['about', 'faq', 'contact'] } }
|
||||||
|
});
|
||||||
|
// 1. Create About Page
|
||||||
|
await prisma.page.upsert({
|
||||||
|
where: { slug: 'about' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
slug: 'about',
|
||||||
|
title: 'About Us',
|
||||||
|
isPublished: true,
|
||||||
|
sections: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
type: 'HERO',
|
||||||
|
order: 1,
|
||||||
|
payload: {
|
||||||
|
tagline: 'Our Story',
|
||||||
|
title: 'Born from a passion for stillness',
|
||||||
|
description: 'HomAroma began in 2019 as a one-person studio in Bangkok, crafting small batches of botanical aroma blends for friends and family. What started as a personal journey toward calm became a philosophy — that our environment shapes how we feel, think, and rest.',
|
||||||
|
align: 'center',
|
||||||
|
imageUrl: '/images/hero.png',
|
||||||
|
overlay: true,
|
||||||
|
buttonText: 'Discover More',
|
||||||
|
buttonLink: '/shop'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'TEXT',
|
||||||
|
order: 2,
|
||||||
|
payload: {
|
||||||
|
tagline: 'Stats',
|
||||||
|
title: 'Company Stats',
|
||||||
|
content: '45+ Unique Scents\n5 Product Lines\nFounded 2019\n100% Natural Extracts',
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'EDITORIAL_TILE',
|
||||||
|
order: 3,
|
||||||
|
payload: {
|
||||||
|
tagline: 'Our Mission',
|
||||||
|
title: 'Transforming spaces into sanctuaries of peace',
|
||||||
|
description: 'We believe that scent is the fastest pathway to emotional transformation. A single breath of the right blend can dissolve anxiety, sharpen focus, or unlock creativity. Our mission is to make that transformation accessible — through products that are as beautiful as they are effective.\n\nEvery HomAroma product is designed with intention: the bottles are reusable, the packaging is recycled, and the formulas are written to be understood by anyone — not hidden behind chemical jargon.',
|
||||||
|
imageUrl: '/images/hero.png',
|
||||||
|
imageAlt: 'HomAroma Studio, Bangkok',
|
||||||
|
reverse: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'TEXT',
|
||||||
|
order: 4,
|
||||||
|
payload: {
|
||||||
|
tagline: 'What We Stand For',
|
||||||
|
title: 'Three Pillars',
|
||||||
|
content: '1. Pure Origins: Every extract is sourced directly from certified organic farms in Northern Thailand, Vietnam, and Southern France. We work exclusively with small-batch artisan growers.\n\n2. Artisan Craft: Each formula is cold-extracted and hand-blended in our Bangkok studio. No heat processing means every aromatic molecule remains intact and therapeutically potent.\n\n3. Zero Compromise: No synthetic fragrance. No parabens. No animal testing. Our commitment to clean formulation is absolute — what touches your senses should honour your wellbeing.',
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'BANNER',
|
||||||
|
order: 5,
|
||||||
|
payload: {
|
||||||
|
title: 'Ready to find your scent?',
|
||||||
|
description: 'Let our Scent Guide help you discover the perfect blend for your moment.',
|
||||||
|
buttonText: 'Take the Scent Guide',
|
||||||
|
buttonLink: '/scent-guide',
|
||||||
|
imageUrl: '/images/hero.png'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 2. Create FAQ Page
|
||||||
|
await prisma.page.upsert({
|
||||||
|
where: { slug: 'faq' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
slug: 'faq',
|
||||||
|
title: 'Frequently Asked Questions',
|
||||||
|
isPublished: true,
|
||||||
|
sections: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
type: 'TEXT',
|
||||||
|
order: 1,
|
||||||
|
payload: {
|
||||||
|
tagline: 'Support',
|
||||||
|
title: 'Frequently Asked Questions',
|
||||||
|
content: 'Everything you need to know about HomAroma products, shipping, and care.',
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ACCORDION',
|
||||||
|
order: 2,
|
||||||
|
payload: {
|
||||||
|
categories: [
|
||||||
|
{
|
||||||
|
name: 'Products',
|
||||||
|
icon: '🌿',
|
||||||
|
questions: [
|
||||||
|
{ q: 'What are the ingredients in your aroma products?', a: 'All HomAroma products are crafted from 100% natural botanical extracts, pure essential oils, and pharmaceutical-grade carrier bases. We never use synthetic fragrances, parabens, or harmful chemicals. Each product label lists full ingredient transparency.' },
|
||||||
|
{ q: 'How long does each product last?', a: 'Our Reed Diffusers last 4–8 weeks depending on room size and airflow. Aroma Oils (30ml) last approximately 3–4 months with normal use. Room Sprays (100ml) provide 60–80 uses. Car Diffusers cartridges last up to 45 days.' },
|
||||||
|
{ q: 'Are your products suitable for sensitive skin or allergies?', a: 'We recommend patch testing before topical use. Our Aroma Oils are formulated for diffuser use primarily. If you have known fragrance allergies, consult our scent guide or contact us with your specific concerns — we are happy to help identify suitable options.' },
|
||||||
|
{ q: 'What is the difference between Aroma Oil and Oil Ozone?', a: 'Aroma Oils are pure essential oil blends designed for aromatherapy diffusers and therapeutic use. Oil Ozone is our premium line featuring ozone-infused botanical extracts — designed for air purification and deeper space transformation, ideal for large rooms and commercial spaces.' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Usage',
|
||||||
|
icon: '💡',
|
||||||
|
questions: [
|
||||||
|
{ q: 'How do I use a Reed Diffuser?', a: 'Remove the stopper, place the reeds into the bottle, and let them absorb the oil for 1 hour before flipping. For optimal fragrance, flip the reeds every 2–3 days. Keep away from direct sunlight and air conditioning vents for longest lasting performance.' },
|
||||||
|
{ q: 'Can I mix different scents?', a: 'Yes! Blending scents is an art form we encourage. A general rule: combine no more than 3 scents at a time, using one dominant (60%), one supporting (30%), and one accent (10%). Our Scent Guide can help you find complementary combinations.' },
|
||||||
|
{ q: 'How long should I run my aroma diffuser?', a: 'For best results and to avoid olfactory fatigue, we recommend 30–60 minute sessions with 30-minute breaks in between. This also extends product life and prevents overwhelming fragrance build-up in small spaces.' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Shipping & Orders',
|
||||||
|
icon: '📦',
|
||||||
|
questions: [
|
||||||
|
{ q: 'How long does shipping take?', a: 'Standard delivery within Bangkok takes 1–2 business days. Provincial orders take 2–4 business days. Express same-day delivery is available in Bangkok for orders placed before 12:00 PM.' },
|
||||||
|
{ q: 'Do you ship internationally?', a: 'Currently we ship within Thailand only. International shipping is planned for late 2025. Sign up for our newsletter to be notified when we expand to your region.' },
|
||||||
|
{ q: 'What is your return policy?', a: 'We accept returns within 14 days of purchase for unopened, undamaged products in original packaging. For defective or damaged products, we offer full replacement at no cost. Contact us at hello@homaroma.shop with your order number.' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Scent Guide',
|
||||||
|
icon: '🧪',
|
||||||
|
questions: [
|
||||||
|
{ q: 'How do I choose the right scent for my space?', a: 'Consider the function of the space first. Bedrooms benefit from calming scents like lavender and chamomile. Living rooms work well with warm, welcoming notes like sandalwood and vanilla. Home offices pair best with focus-enhancing eucalyptus and citrus. Use our interactive Scent Guide for personalized recommendations.' },
|
||||||
|
{ q: 'Can I request custom scent blending?', a: 'Yes! Our bespoke blending service is available for orders of 6 units or more. Contact us at least 10 business days before your desired delivery date. Custom blending fees start at ฿500 per unique formula.' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'BANNER',
|
||||||
|
order: 3,
|
||||||
|
payload: {
|
||||||
|
title: 'Still have questions?',
|
||||||
|
description: 'Our team is here to help — usually responds within 2 hours.',
|
||||||
|
buttonText: 'Contact Us',
|
||||||
|
buttonLink: '/contact',
|
||||||
|
imageUrl: '/images/hero.png'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 3. Create Contact Page
|
||||||
|
await prisma.page.upsert({
|
||||||
|
where: { slug: 'contact' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
slug: 'contact',
|
||||||
|
title: 'Contact Us',
|
||||||
|
isPublished: true,
|
||||||
|
sections: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
type: 'TEXT',
|
||||||
|
order: 1,
|
||||||
|
payload: {
|
||||||
|
tagline: 'Get in Touch',
|
||||||
|
title: 'Contact Us',
|
||||||
|
content: 'Have a question, custom request, or partnership idea? We\'d love to hear from you.',
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'CONTACT_INFO',
|
||||||
|
order: 2,
|
||||||
|
payload: {
|
||||||
|
address: 'Bangkok, Thailand',
|
||||||
|
email: 'hello@homaroma.shop',
|
||||||
|
line: '@homaroma',
|
||||||
|
hours: 'Mon–Sat, 9:00–18:00 (ICT)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'CONTACT_FORM',
|
||||||
|
order: 3,
|
||||||
|
payload: {
|
||||||
|
subjects: ['Product Inquiry', 'Order Support', 'Custom Blending', 'Wholesale', 'Partnership', 'Other'],
|
||||||
|
buttonText: 'Send Message'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('Pages created successfully.');
|
||||||
|
}
|
||||||
|
main()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
|
//# sourceMappingURL=migrate-pages.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"migrate-pages.js","sourceRoot":"","sources":["migrate-pages.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,mBAAmB,CAAC;AAEvC,KAAK,UAAU,IAAI;IACjB,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;IAEzC,gDAAgD;IAChD,MAAM,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC;QAC3B,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,EAAE;KACrD,CAAC,CAAC;IAEH,uBAAuB;IACvB,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;QACvB,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;QACxB,MAAM,EAAE,EAAE;QACV,MAAM,EAAE;YACN,IAAI,EAAE,OAAO;YACb,KAAK,EAAE,UAAU;YACjB,WAAW,EAAE,IAAI;YACjB,QAAQ,EAAE;gBACR,MAAM,EAAE;oBACN;wBACE,IAAI,EAAE,MAAM;wBACZ,KAAK,EAAE,CAAC;wBACR,OAAO,EAAE;4BACP,OAAO,EAAE,WAAW;4BACpB,KAAK,EAAE,mCAAmC;4BAC1C,WAAW,EAAE,mQAAmQ;4BAChR,KAAK,EAAE,QAAQ;4BACf,QAAQ,EAAE,kBAAkB;4BAC5B,OAAO,EAAE,IAAI;4BACb,UAAU,EAAE,eAAe;4BAC3B,UAAU,EAAE,OAAO;yBACpB;qBACF;oBACD;wBACE,IAAI,EAAE,MAAM;wBACZ,KAAK,EAAE,CAAC;wBACR,OAAO,EAAE;4BACP,OAAO,EAAE,OAAO;4BAChB,KAAK,EAAE,eAAe;4BACtB,OAAO,EAAE,yEAAyE;4BAClF,KAAK,EAAE,QAAQ;yBAChB;qBACF;oBACD;wBACE,IAAI,EAAE,gBAAgB;wBACtB,KAAK,EAAE,CAAC;wBACR,OAAO,EAAE;4BACP,OAAO,EAAE,aAAa;4BACtB,KAAK,EAAE,+CAA+C;4BACtD,WAAW,EAAE,ueAAue;4BACpf,QAAQ,EAAE,kBAAkB;4BAC5B,QAAQ,EAAE,0BAA0B;4BACpC,OAAO,EAAE,KAAK;yBACf;qBACF;oBACD;wBACE,IAAI,EAAE,MAAM;wBACZ,KAAK,EAAE,CAAC;wBACR,OAAO,EAAE;4BACP,OAAO,EAAE,mBAAmB;4BAC5B,KAAK,EAAE,eAAe;4BACtB,OAAO,EAAE,2iBAA2iB;4BACpjB,KAAK,EAAE,QAAQ;yBAChB;qBACF;oBACD;wBACE,IAAI,EAAE,QAAQ;wBACd,KAAK,EAAE,CAAC;wBACR,OAAO,EAAE;4BACP,KAAK,EAAE,2BAA2B;4BAClC,WAAW,EAAE,0EAA0E;4BACvF,UAAU,EAAE,sBAAsB;4BAClC,UAAU,EAAE,cAAc;4BAC1B,QAAQ,EAAE,kBAAkB;yBAC7B;qBACF;iBACF;aACF;SACF;KACF,CAAC,CAAC;IAEH,qBAAqB;IACrB,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;QACvB,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE;QACtB,MAAM,EAAE,EAAE;QACV,MAAM,EAAE;YACN,IAAI,EAAE,KAAK;YACX,KAAK,EAAE,4BAA4B;YACnC,WAAW,EAAE,IAAI;YACjB,QAAQ,EAAE;gBACR,MAAM,EAAE;oBACN;wBACE,IAAI,EAAE,MAAM;wBACZ,KAAK,EAAE,CAAC;wBACR,OAAO,EAAE;4BACP,OAAO,EAAE,SAAS;4BAClB,KAAK,EAAE,4BAA4B;4BACnC,OAAO,EAAE,0EAA0E;4BACnF,KAAK,EAAE,QAAQ;yBAChB;qBACF;oBACD;wBACE,IAAI,EAAE,WAAW;wBACjB,KAAK,EAAE,CAAC;wBACR,OAAO,EAAE;4BACP,UAAU,EAAE;gCACV;oCACE,IAAI,EAAE,UAAU;oCAChB,IAAI,EAAE,IAAI;oCACV,SAAS,EAAE;wCACT,EAAE,CAAC,EAAE,kDAAkD,EAAE,CAAC,EAAE,gQAAgQ,EAAE;wCAC9T,EAAE,CAAC,EAAE,kCAAkC,EAAE,CAAC,EAAE,6NAA6N,EAAE;wCAC3Q,EAAE,CAAC,EAAE,6DAA6D,EAAE,CAAC,EAAE,qQAAqQ,EAAE;wCAC9U,EAAE,CAAC,EAAE,yDAAyD,EAAE,CAAC,EAAE,0RAA0R,EAAE;qCAChW;iCACF;gCACD;oCACE,IAAI,EAAE,OAAO;oCACb,IAAI,EAAE,IAAI;oCACV,SAAS,EAAE;wCACT,EAAE,CAAC,EAAE,+BAA+B,EAAE,CAAC,EAAE,+PAA+P,EAAE;wCAC1S,EAAE,CAAC,EAAE,6BAA6B,EAAE,CAAC,EAAE,gPAAgP,EAAE;wCACzR,EAAE,CAAC,EAAE,0CAA0C,EAAE,CAAC,EAAE,oNAAoN,EAAE;qCAC3Q;iCACF;gCACD;oCACE,IAAI,EAAE,mBAAmB;oCACzB,IAAI,EAAE,IAAI;oCACV,SAAS,EAAE;wCACT,EAAE,CAAC,EAAE,8BAA8B,EAAE,CAAC,EAAE,0LAA0L,EAAE;wCACpO,EAAE,CAAC,EAAE,8BAA8B,EAAE,CAAC,EAAE,mKAAmK,EAAE;wCAC7M,EAAE,CAAC,EAAE,6BAA6B,EAAE,CAAC,EAAE,yOAAyO,EAAE;qCACnR;iCACF;gCACD;oCACE,IAAI,EAAE,aAAa;oCACnB,IAAI,EAAE,IAAI;oCACV,SAAS,EAAE;wCACT,EAAE,CAAC,EAAE,+CAA+C,EAAE,CAAC,EAAE,iUAAiU,EAAE;wCAC5X,EAAE,CAAC,EAAE,sCAAsC,EAAE,CAAC,EAAE,8MAA8M,EAAE;qCACjQ;iCACF;6BACF;yBACF;qBACF;oBACD;wBACE,IAAI,EAAE,QAAQ;wBACd,KAAK,EAAE,CAAC;wBACR,OAAO,EAAE;4BACP,KAAK,EAAE,uBAAuB;4BAC9B,WAAW,EAAE,6DAA6D;4BAC1E,UAAU,EAAE,YAAY;4BACxB,UAAU,EAAE,UAAU;4BACtB,QAAQ,EAAE,kBAAkB;yBAC7B;qBACF;iBACF;aACF;SACF;KACF,CAAC,CAAC;IAEH,yBAAyB;IACzB,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;QACvB,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;QAC1B,MAAM,EAAE,EAAE;QACV,MAAM,EAAE;YACN,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,YAAY;YACnB,WAAW,EAAE,IAAI;YACjB,QAAQ,EAAE;gBACR,MAAM,EAAE;oBACN;wBACE,IAAI,EAAE,MAAM;wBACZ,KAAK,EAAE,CAAC;wBACR,OAAO,EAAE;4BACP,OAAO,EAAE,cAAc;4BACvB,KAAK,EAAE,YAAY;4BACnB,OAAO,EAAE,oFAAoF;4BAC7F,KAAK,EAAE,QAAQ;yBAChB;qBACF;oBACD;wBACE,IAAI,EAAE,cAAc;wBACpB,KAAK,EAAE,CAAC;wBACR,OAAO,EAAE;4BACP,OAAO,EAAE,mBAAmB;4BAC5B,KAAK,EAAE,qBAAqB;4BAC5B,IAAI,EAAE,WAAW;4BACjB,KAAK,EAAE,2BAA2B;yBACnC;qBACF;oBACD;wBACE,IAAI,EAAE,cAAc;wBACpB,KAAK,EAAE,CAAC;wBACR,OAAO,EAAE;4BACP,QAAQ,EAAE,CAAC,iBAAiB,EAAE,eAAe,EAAE,iBAAiB,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,CAAC;4BACtG,UAAU,EAAE,cAAc;yBAC3B;qBACF;iBACF;aACF;SACF;KACF,CAAC,CAAC;IAEH,OAAO,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAC;AAC7C,CAAC;AAED,IAAI,EAAE;KACH,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE;IACX,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACjB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC;KACD,OAAO,CAAC,KAAK,IAAI,EAAE;IAClB,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC;AAC7B,CAAC,CAAC,CAAC"}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export {};
|
||||||
|
//# sourceMappingURL=migrate-production.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"migrate-production.d.ts","sourceRoot":"","sources":["migrate-production.ts"],"names":[],"mappings":""}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
const BACKEND_URL = "https://api.homaroma.shop/api";
|
||||||
|
const ADMIN_USERNAME = "admin";
|
||||||
|
const ADMIN_PASSWORD = "homaroma123";
|
||||||
|
async function main() {
|
||||||
|
console.log("Authenticating...");
|
||||||
|
const authRes = await axios.post(`${BACKEND_URL}/auth/login`, {
|
||||||
|
username: ADMIN_USERNAME,
|
||||||
|
password: ADMIN_PASSWORD,
|
||||||
|
});
|
||||||
|
const token = authRes.data.token;
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: BACKEND_URL,
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
const pages = [
|
||||||
|
{
|
||||||
|
slug: 'about', title: 'About Us', isPublished: true,
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
type: 'HERO', order: 1,
|
||||||
|
payload: { tagline: 'Our Story', title: 'Born from a passion for stillness', description: 'HomAroma began in 2019 as a one-person studio in Bangkok, crafting small batches of botanical aroma blends for friends and family. What started as a personal journey toward calm became a philosophy — that our environment shapes how we feel, think, and rest.', align: 'center', bgImage: '', overlay: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'TEXT', order: 2,
|
||||||
|
payload: { tagline: 'Stats', title: 'Company Stats', content: '45+ Unique Scents\n5 Product Lines\nFounded 2019\n100% Natural Extracts', align: 'center' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'EDITORIAL_TILE', order: 3,
|
||||||
|
payload: { tagline: 'Our Mission', title: 'Transforming spaces into sanctuaries of peace', content: 'We believe that scent is the fastest pathway to emotional transformation. A single breath of the right blend can dissolve anxiety, sharpen focus, or unlock creativity. Our mission is to make that transformation accessible — through products that are as beautiful as they are effective.\n\nEvery HomAroma product is designed with intention: the bottles are reusable, the packaging is recycled, and the formulas are written to be understood by anyone — not hidden behind chemical jargon.', imageSrc: '', imageAlt: 'HomAroma Studio, Bangkok', reverse: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'TEXT', order: 4,
|
||||||
|
payload: { tagline: 'What We Stand For', title: 'Three Pillars', content: '1. Pure Origins: Every extract is sourced directly from certified organic farms in Northern Thailand, Vietnam, and Southern France. We work exclusively with small-batch artisan growers.\n\n2. Artisan Craft: Each formula is cold-extracted and hand-blended in our Bangkok studio. No heat processing means every aromatic molecule remains intact and therapeutically potent.\n\n3. Zero Compromise: No synthetic fragrance. No parabens. No animal testing. Our commitment to clean formulation is absolute — what touches your senses should honour your wellbeing.', align: 'center' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'BANNER', order: 5,
|
||||||
|
payload: { title: 'Ready to find your scent?', description: 'Let our Scent Guide help you discover the perfect blend for your moment.', buttonText: 'Take the Scent Guide', buttonLink: '/scent-guide' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'faq', title: 'Frequently Asked Questions', isPublished: true,
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
type: 'TEXT', order: 1,
|
||||||
|
payload: { tagline: 'Support', title: 'Frequently Asked Questions', content: 'Everything you need to know about HomAroma products, shipping, and care.', align: 'center' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ACCORDION', order: 2,
|
||||||
|
payload: { categories: [{ name: 'Products', icon: '🌿', questions: [{ q: 'Are your products 100% natural?', a: 'Yes. All our products are formulated using 100% natural botanical extracts, pure essential oils, and plant-derived carriers. We never use synthetic fragrances or phthalates.' }, { q: 'Are your products pet-friendly?', a: 'While natural, some essential oils can be sensitive to certain pets (like cats and eucalyptus). Please consult your vet, and always ensure your pet can leave the room when a diffuser is running.' }] }, { name: 'Shipping', icon: '📦', questions: [{ q: 'Do you ship internationally?', a: 'Currently, we ship within Thailand and selected countries in Southeast Asia. International shipping expansion is planned for late 2026.' }, { q: 'How long does shipping take?', a: 'Standard domestic shipping within Thailand takes 2-3 business days. Express shipping takes 1-2 business days.' }] }] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'BANNER', order: 3,
|
||||||
|
payload: { title: 'Still have questions?', description: 'Our support team is available Mon-Sat, 9am - 6pm.', buttonText: 'Contact Us', buttonLink: '/contact' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'contact', title: 'Contact Us', isPublished: true,
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
type: 'TEXT', order: 1,
|
||||||
|
payload: { tagline: 'Get in Touch', title: 'We are here to help', content: 'Whether you need help selecting a scent or have questions about an order, our team is ready to assist you.', align: 'center' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'CONTACT_INFO', order: 2,
|
||||||
|
payload: { address: 'HomAroma Studio\n123 Sukhumvit Road\nBangkok 10110, Thailand', email: 'hello@homaroma.shop', line: '@homaroma', hours: 'Mon-Sat: 9:00 AM - 6:00 PM' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
for (const page of pages) {
|
||||||
|
console.log(`Creating page: ${page.slug}`);
|
||||||
|
try {
|
||||||
|
const pageRes = await api.post('/pages', { slug: page.slug, title: page.title, isPublished: page.isPublished });
|
||||||
|
const pageId = pageRes.data.id;
|
||||||
|
console.log(`Created page ${page.slug} with ID ${pageId}`);
|
||||||
|
for (const section of page.sections) {
|
||||||
|
console.log(` Adding section ${section.type}`);
|
||||||
|
await api.post(`/pages/${pageId}/sections`, { type: section.type, order: section.order, payload: section.payload, isActive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(`Error on ${page.slug}:`, err.response?.data || err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Migration to Production complete!");
|
||||||
|
}
|
||||||
|
main();
|
||||||
|
//# sourceMappingURL=migrate-production.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"migrate-production.js","sourceRoot":"","sources":["migrate-production.ts"],"names":[],"mappings":"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"}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { Elysia } from "elysia";
|
||||||
|
import { cors } from "@elysiajs/cors";
|
||||||
|
import { swagger } from "@elysiajs/swagger";
|
||||||
|
import { jwt } from "@elysiajs/jwt";
|
||||||
|
import { staticPlugin } from "@elysiajs/static";
|
||||||
|
import { categoriesRoute } from "./routes/categories";
|
||||||
|
import { productsRoute } from "./routes/products";
|
||||||
|
import { authRoute } from "./routes/auth";
|
||||||
|
import { uploadRoute } from "./routes/upload";
|
||||||
|
import { heroRoute } from "./routes/hero";
|
||||||
|
import { ordersRoute } from "./routes/orders";
|
||||||
|
import { settingsRoute } from "./routes/settings";
|
||||||
|
import { articlesRoute } from "./routes/articles";
|
||||||
|
import { tiktokRoute } from "./routes/tiktok";
|
||||||
|
import { rawMaterialsRoute } from "./routes/raw-materials";
|
||||||
|
import { shopeeRoute } from "./routes/shopee";
|
||||||
|
import { tiktokOrderRoute } from "./routes/tiktokOrder";
|
||||||
|
import { lazadaOrderRoute } from "./routes/lazadaOrder";
|
||||||
|
import { customersRoute } from "./routes/customers";
|
||||||
|
import { couponsRoute } from "./routes/coupons";
|
||||||
|
import { reviewsRoute } from "./routes/reviews";
|
||||||
|
import { promotionsRoute } from "./routes/promotions";
|
||||||
|
import { pagesRoute } from "./routes/pages";
|
||||||
|
import { paymentsRoute } from "./routes/payments";
|
||||||
|
import { profileRoute } from "./routes/profile";
|
||||||
|
import { lineRoutes } from "./routes/line";
|
||||||
|
import { menusRoute } from "./routes/menus";
|
||||||
|
import { newsletterRoute } from "./routes/newsletter";
|
||||||
|
import { mediaRoute } from "./routes/media";
|
||||||
|
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3051;
|
||||||
|
const CORS_ORIGIN = process.env.CORS_ORIGIN || "http://localhost:5173";
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || "homaroma_secret_2026_scent";
|
||||||
|
const app = new Elysia()
|
||||||
|
.onParse(async ({ request }) => {
|
||||||
|
console.log('[onParse] Incoming request URL:', request.url);
|
||||||
|
if (request.url.includes('/api/line/webhook')) {
|
||||||
|
const text = await request.text();
|
||||||
|
console.log('[onParse] Intercepted LINE webhook raw body length:', text.length);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.use(cors({
|
||||||
|
origin: [
|
||||||
|
...CORS_ORIGIN.split(",").map((o) => o.trim()),
|
||||||
|
"https://admin.homaroma.shop",
|
||||||
|
"https://spa.homaroma.shop", // LIFF (SPA)
|
||||||
|
"https://homaroma.shop", // Main Website
|
||||||
|
"http://localhost:5174", // Admin
|
||||||
|
"http://localhost:4173",
|
||||||
|
"http://localhost:3001", // SSR Next.js
|
||||||
|
"https://react5173.kitizone.com",
|
||||||
|
"https://elysia3051.kitizone.com"
|
||||||
|
],
|
||||||
|
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
||||||
|
credentials: true,
|
||||||
|
}))
|
||||||
|
.use(jwt({
|
||||||
|
name: "jwt",
|
||||||
|
secret: JWT_SECRET,
|
||||||
|
exp: "24h",
|
||||||
|
}))
|
||||||
|
.use(swagger({
|
||||||
|
documentation: {
|
||||||
|
info: {
|
||||||
|
title: "HomAroma API",
|
||||||
|
version: "1.0.0",
|
||||||
|
description: "API for HomAroma & Essential Oil Products",
|
||||||
|
},
|
||||||
|
tags: [
|
||||||
|
{ name: "categories", description: "Product categories" },
|
||||||
|
{ name: "products", description: "Products and variants" },
|
||||||
|
{ name: "auth", description: "Authentication for Admin" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.use(staticPlugin({
|
||||||
|
assets: "public",
|
||||||
|
prefix: "", // use empty string for root
|
||||||
|
}))
|
||||||
|
.get("/", () => ({
|
||||||
|
name: "HomAroma API",
|
||||||
|
version: "1.0.0",
|
||||||
|
status: "running",
|
||||||
|
docs: `/swagger`,
|
||||||
|
}))
|
||||||
|
// Explicit route for uploads to guarantee it works even if staticPlugin fails
|
||||||
|
.get("/uploads/:filename", ({ params: { filename }, set }) => {
|
||||||
|
const file = Bun.file(`public/uploads/${filename}`);
|
||||||
|
if (file.size === 0) {
|
||||||
|
set.status = 404;
|
||||||
|
return "File not found";
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
})
|
||||||
|
.group("/api", (app) => app.use(authRoute).use(categoriesRoute).use(productsRoute).use(uploadRoute).use(heroRoute).use(ordersRoute).use(settingsRoute).use(articlesRoute).use(tiktokRoute).use(rawMaterialsRoute).use(shopeeRoute).use(tiktokOrderRoute).use(lazadaOrderRoute).use(customersRoute).use(couponsRoute).use(reviewsRoute).use(promotionsRoute).use(pagesRoute).use(paymentsRoute).use(profileRoute).use(lineRoutes).use(menusRoute).use(newsletterRoute).use(mediaRoute))
|
||||||
|
.listen({ port: PORT, hostname: "0.0.0.0" });
|
||||||
|
console.log(`🌿 HomAroma API is running at http://${app.server?.hostname}:${app.server?.port}`);
|
||||||
|
console.log(`📖 Swagger docs: http://${app.server?.hostname}:${app.server?.port}/swagger`);
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import * as line from '@line/bot-sdk';
|
||||||
|
const channelAccessToken = process.env.LINE_CHANNEL_ACCESS_TOKEN || '';
|
||||||
|
const channelSecret = process.env.LINE_CHANNEL_SECRET || '';
|
||||||
|
export const lineConfig = {
|
||||||
|
channelAccessToken,
|
||||||
|
channelSecret,
|
||||||
|
};
|
||||||
|
export const lineClient = new line.messagingApi.MessagingApiClient({
|
||||||
|
channelAccessToken,
|
||||||
|
});
|
||||||
|
//# sourceMappingURL=lineClient.js.map
|
||||||
@@ -0,0 +1,554 @@
|
|||||||
|
import prisma from "./prisma";
|
||||||
|
import { lineClient } from "./lineClient";
|
||||||
|
export async function sendOrderCreatedNotification(orderId) {
|
||||||
|
try {
|
||||||
|
const order = await prisma.order.findUnique({
|
||||||
|
where: { id: orderId },
|
||||||
|
include: { items: true, customer: true }
|
||||||
|
});
|
||||||
|
if (!order || !order.customer?.lineUserId) {
|
||||||
|
console.log(`[Notification] Order #${orderId} has no associated lineUserId or does not exist.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const settings = await prisma.storeSettings.findFirst();
|
||||||
|
const bankName = settings?.bankName || "KASIKORNBANK (KBANK)";
|
||||||
|
const bankAccountNumber = settings?.bankAccountNumber || "012-3-45678-9";
|
||||||
|
const bankAccountName = settings?.bankAccountName || "HomAroma Co., Ltd.";
|
||||||
|
const itemContents = order.items.map(item => ({
|
||||||
|
type: "box",
|
||||||
|
layout: "horizontal",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `${item.productName}${item.variantSize ? ` (${item.variantSize})` : ""} x ${item.quantity}`,
|
||||||
|
size: "sm",
|
||||||
|
color: "#333333",
|
||||||
|
flex: 3,
|
||||||
|
wrap: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `฿${item.priceAtPurchase * item.quantity}`,
|
||||||
|
size: "sm",
|
||||||
|
color: "#1C3F3A",
|
||||||
|
weight: "bold",
|
||||||
|
align: "end",
|
||||||
|
flex: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
const flexMessage = {
|
||||||
|
type: "flex",
|
||||||
|
altText: `HomAroma: คำสั่งซื้อ #${order.id} ได้รับแล้ว กรุณาชำระเงิน`,
|
||||||
|
contents: {
|
||||||
|
type: "bubble",
|
||||||
|
header: {
|
||||||
|
type: "box",
|
||||||
|
layout: "vertical",
|
||||||
|
backgroundColor: "#1C3F3A",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "HomAroma Official",
|
||||||
|
color: "#D4B16A",
|
||||||
|
weight: "bold",
|
||||||
|
size: "xs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "ได้รับคำสั่งซื้อของคุณแล้ว",
|
||||||
|
color: "#FFFFFF",
|
||||||
|
weight: "bold",
|
||||||
|
size: "md",
|
||||||
|
margin: "sm"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
type: "box",
|
||||||
|
layout: "vertical",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `หมายเลขออเดอร์: #HM-${order.id}`,
|
||||||
|
weight: "bold",
|
||||||
|
size: "sm",
|
||||||
|
color: "#333333"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "separator",
|
||||||
|
margin: "md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "box",
|
||||||
|
layout: "vertical",
|
||||||
|
margin: "md",
|
||||||
|
spacing: "sm",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "box",
|
||||||
|
layout: "horizontal",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "รายการสินค้า",
|
||||||
|
size: "xs",
|
||||||
|
color: "#888888",
|
||||||
|
flex: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "ยอดรวม",
|
||||||
|
size: "xs",
|
||||||
|
color: "#888888",
|
||||||
|
align: "end",
|
||||||
|
flex: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
...itemContents
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "separator",
|
||||||
|
margin: "md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "box",
|
||||||
|
layout: "horizontal",
|
||||||
|
margin: "md",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "ยอดชำระสุทธิ (รวมค่าส่ง)",
|
||||||
|
size: "sm",
|
||||||
|
color: "#555555",
|
||||||
|
weight: "bold",
|
||||||
|
flex: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `฿${order.totalAmount}`,
|
||||||
|
size: "sm",
|
||||||
|
color: "#1C3F3A",
|
||||||
|
weight: "bold",
|
||||||
|
align: "end",
|
||||||
|
flex: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "separator",
|
||||||
|
margin: "md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "box",
|
||||||
|
layout: "vertical",
|
||||||
|
margin: "md",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "🏦 ช่องทางการชำระเงินโอนเงิน:",
|
||||||
|
size: "xs",
|
||||||
|
color: "#555555",
|
||||||
|
weight: "bold"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `${bankName} (${bankAccountNumber})`,
|
||||||
|
size: "xs",
|
||||||
|
color: "#1C3F3A",
|
||||||
|
margin: "xs",
|
||||||
|
weight: "bold"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `ชื่อบัญชี: ${bankAccountName}`,
|
||||||
|
size: "xs",
|
||||||
|
color: "#555555"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
type: "box",
|
||||||
|
layout: "vertical",
|
||||||
|
spacing: "sm",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
style: "primary",
|
||||||
|
color: "#1C3F3A",
|
||||||
|
action: {
|
||||||
|
"type": "uri",
|
||||||
|
"label": "แนบสลิปชำระเงิน",
|
||||||
|
"uri": `https://spa.homaroma.shop/order-success?id=${order.id}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
style: "link",
|
||||||
|
height: "sm",
|
||||||
|
action: {
|
||||||
|
"type": "uri",
|
||||||
|
"label": "ติดตามสถานะออเดอร์",
|
||||||
|
"uri": "https://spa.homaroma.shop/track-order"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await lineClient.pushMessage({
|
||||||
|
to: order.customer.lineUserId,
|
||||||
|
messages: [flexMessage]
|
||||||
|
});
|
||||||
|
console.log(`[Notification] OrderCreated notification sent for Order #${orderId}`);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(`[Notification] Failed to send OrderCreated notification for Order #${orderId}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function sendPaymentConfirmedNotification(orderId) {
|
||||||
|
try {
|
||||||
|
const order = await prisma.order.findUnique({
|
||||||
|
where: { id: orderId },
|
||||||
|
include: { customer: true }
|
||||||
|
});
|
||||||
|
if (!order || !order.customer?.lineUserId) {
|
||||||
|
console.log(`[Notification] Order #${orderId} has no associated lineUserId or does not exist.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const flexMessage = {
|
||||||
|
type: "flex",
|
||||||
|
altText: "HomAroma: ได้รับการชำระเงินเรียบร้อยแล้ว",
|
||||||
|
contents: {
|
||||||
|
type: "bubble",
|
||||||
|
body: {
|
||||||
|
type: "box",
|
||||||
|
layout: "vertical",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "box",
|
||||||
|
layout: "vertical",
|
||||||
|
alignItems: "center",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "✨ ชำระเงินสำเร็จ ✨",
|
||||||
|
weight: "bold",
|
||||||
|
size: "xl",
|
||||||
|
color: "#1C3F3A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `ขอขอบคุณสำหรับคำสั่งซื้อออเดอร์ #HM-${order.id}`,
|
||||||
|
size: "xs",
|
||||||
|
color: "#888888",
|
||||||
|
margin: "xs"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "box",
|
||||||
|
layout: "vertical",
|
||||||
|
backgroundColor: "#F4F6F5",
|
||||||
|
margin: "lg",
|
||||||
|
paddingAll: "md",
|
||||||
|
cornerRadius: "md",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "box",
|
||||||
|
layout: "horizontal",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "ได้รับแต้มครั้งนี้",
|
||||||
|
size: "sm",
|
||||||
|
color: "#555555"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `+${order.pointsEarned} แต้ม`,
|
||||||
|
weight: "bold",
|
||||||
|
size: "sm",
|
||||||
|
color: "#1C3F3A",
|
||||||
|
align: "end"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "box",
|
||||||
|
layout: "horizontal",
|
||||||
|
margin: "xs",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "แต้มสะสมรวมล่าสุด",
|
||||||
|
size: "sm",
|
||||||
|
color: "#555555"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `${order.customer.points} แต้ม`,
|
||||||
|
weight: "bold",
|
||||||
|
size: "sm",
|
||||||
|
color: "#D4B16A",
|
||||||
|
align: "end"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
type: "box",
|
||||||
|
layout: "vertical",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
style: "primary",
|
||||||
|
color: "#1C3F3A",
|
||||||
|
action: {
|
||||||
|
"type": "uri",
|
||||||
|
"label": "แลกของรางวัล & คูปองสะสม",
|
||||||
|
"uri": "https://spa.homaroma.shop/profile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await lineClient.pushMessage({
|
||||||
|
to: order.customer.lineUserId,
|
||||||
|
messages: [flexMessage]
|
||||||
|
});
|
||||||
|
console.log(`[Notification] PaymentConfirmed notification sent for Order #${orderId}`);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(`[Notification] Failed to send PaymentConfirmed notification for Order #${orderId}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function sendOrderShippedNotification(orderId) {
|
||||||
|
try {
|
||||||
|
const order = await prisma.order.findUnique({
|
||||||
|
where: { id: orderId },
|
||||||
|
include: { customer: true }
|
||||||
|
});
|
||||||
|
if (!order || !order.customer?.lineUserId) {
|
||||||
|
console.log(`[Notification] Order #${orderId} has no associated lineUserId or does not exist.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const flexMessage = {
|
||||||
|
type: "flex",
|
||||||
|
altText: "HomAroma: สินค้าของคุณอยู่ระหว่างจัดส่ง",
|
||||||
|
contents: {
|
||||||
|
type: "bubble",
|
||||||
|
header: {
|
||||||
|
type: "box",
|
||||||
|
layout: "vertical",
|
||||||
|
backgroundColor: "#1C3F3A",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "ออเดอร์ของคุณกำลังเดินทาง 🚚",
|
||||||
|
color: "#FFFFFF",
|
||||||
|
weight: "bold",
|
||||||
|
size: "md"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
type: "box",
|
||||||
|
layout: "vertical",
|
||||||
|
spacing: "md",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "box",
|
||||||
|
layout: "horizontal",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "หมายเลขออเดอร์:",
|
||||||
|
size: "sm",
|
||||||
|
color: "#888888",
|
||||||
|
flex: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `#HM-${order.id}`,
|
||||||
|
size: "sm",
|
||||||
|
color: "#333333",
|
||||||
|
weight: "bold",
|
||||||
|
flex: 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "box",
|
||||||
|
layout: "horizontal",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "หมายเลขพัสดุ:",
|
||||||
|
size: "sm",
|
||||||
|
color: "#888888",
|
||||||
|
flex: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: order.trackingNo || "อยู่ระหว่างเข้ารับพัสดุ",
|
||||||
|
size: "sm",
|
||||||
|
color: "#1C3F3A",
|
||||||
|
weight: "bold",
|
||||||
|
flex: 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
type: "box",
|
||||||
|
layout: "vertical",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
style: "primary",
|
||||||
|
color: "#1C3F3A",
|
||||||
|
action: {
|
||||||
|
"type": "uri",
|
||||||
|
"label": "เช็คสถานะการจัดส่ง",
|
||||||
|
"uri": "https://spa.homaroma.shop/track-order"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await lineClient.pushMessage({
|
||||||
|
to: order.customer.lineUserId,
|
||||||
|
messages: [flexMessage]
|
||||||
|
});
|
||||||
|
console.log(`[Notification] OrderShipped notification sent for Order #${orderId}`);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(`[Notification] Failed to send OrderShipped notification for Order #${orderId}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function sendOrderCancelledNotification(orderId) {
|
||||||
|
try {
|
||||||
|
const order = await prisma.order.findUnique({
|
||||||
|
where: { id: orderId },
|
||||||
|
include: { customer: true }
|
||||||
|
});
|
||||||
|
if (!order || !order.customer?.lineUserId) {
|
||||||
|
console.log(`[Notification] Order #${orderId} has no associated lineUserId or does not exist.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const flexMessage = {
|
||||||
|
type: "flex",
|
||||||
|
altText: "HomAroma: คำสั่งซื้อของคุณถูกยกเลิกแล้ว",
|
||||||
|
contents: {
|
||||||
|
type: "bubble",
|
||||||
|
header: {
|
||||||
|
type: "box",
|
||||||
|
layout: "vertical",
|
||||||
|
backgroundColor: "#8C2A2A",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "คำสั่งซื้อถูกยกเลิก 🚫",
|
||||||
|
color: "#FFFFFF",
|
||||||
|
weight: "bold",
|
||||||
|
size: "md"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
type: "box",
|
||||||
|
layout: "vertical",
|
||||||
|
spacing: "md",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `คำสั่งซื้อหมายเลข #HM-${order.id} ได้ถูกยกเลิกแล้ว เนื่องจากเกินกำหนดเวลาชำระเงิน หรือสิทธิประโยชน์บางประการ หรือตามความประสงค์ของคุณ`,
|
||||||
|
size: "sm",
|
||||||
|
color: "#333333",
|
||||||
|
wrap: true
|
||||||
|
},
|
||||||
|
...(order.pointsUsed > 0 ? [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `* ระบบได้ทำการคืนคะแนนสะสมจำนวน ${order.pointsUsed} แต้มเข้าบัญชีของคุณเรียบร้อยแล้ว`,
|
||||||
|
size: "xs",
|
||||||
|
color: "#8C2A2A",
|
||||||
|
weight: "bold",
|
||||||
|
margin: "md"
|
||||||
|
}
|
||||||
|
] : [])
|
||||||
|
]
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
type: "box",
|
||||||
|
layout: "vertical",
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
style: "primary",
|
||||||
|
color: "#1C3F3A",
|
||||||
|
action: {
|
||||||
|
"type": "uri",
|
||||||
|
"label": "ช้อปต่ออีกครั้ง",
|
||||||
|
"uri": "https://spa.homaroma.shop/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await lineClient.pushMessage({
|
||||||
|
to: order.customer.lineUserId,
|
||||||
|
messages: [flexMessage]
|
||||||
|
});
|
||||||
|
console.log(`[Notification] OrderCancelled notification sent for Order #${orderId}`);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(`[Notification] Failed to send OrderCancelled notification for Order #${orderId}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function markOrderAsPaid(orderId) {
|
||||||
|
try {
|
||||||
|
const order = await prisma.order.findUnique({
|
||||||
|
where: { id: orderId }
|
||||||
|
});
|
||||||
|
if (!order) {
|
||||||
|
console.log(`[Payment] Order #${orderId} not found in markOrderAsPaid.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (order.status === "PAID") {
|
||||||
|
console.log(`[Payment] Order #${orderId} is already PAID.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 1. Update status to PAID
|
||||||
|
await prisma.order.update({
|
||||||
|
where: { id: orderId },
|
||||||
|
data: { status: "PAID" }
|
||||||
|
});
|
||||||
|
// 2. Grant points
|
||||||
|
if (order.customerId && order.pointsEarned > 0) {
|
||||||
|
await prisma.customer.update({
|
||||||
|
where: { id: order.customerId },
|
||||||
|
data: { points: { increment: order.pointsEarned } }
|
||||||
|
});
|
||||||
|
console.log(`[Payment] Granted ${order.pointsEarned} points to Customer #${order.customerId} for Order #${orderId}`);
|
||||||
|
}
|
||||||
|
// 3. Send payment notification
|
||||||
|
await sendPaymentConfirmedNotification(orderId);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(`[Payment] Error in markOrderAsPaid for Order #${orderId}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=notifications.js.map
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"prisma.d.ts","sourceRoot":"","sources":["prisma.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAS9C,QAAA,MAAM,MAAM;;8DAAgC,CAAC;AAE7C,eAAe,MAAM,CAAC"}
|
{"version":3,"file":"prisma.d.ts","sourceRoot":"","sources":["prisma.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAW9C,QAAA,MAAM,MAAM;;8DAAgC,CAAC;AAE7C,eAAe,MAAM,CAAC"}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import { PrismaPg } from "@prisma/adapter-pg";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
dotenv.config();
|
||||||
|
const connectionString = process.env.DATABASE_URL;
|
||||||
|
const pool = new Pool({ connectionString });
|
||||||
|
const adapter = new PrismaPg(pool);
|
||||||
|
const prisma = new PrismaClient({ adapter });
|
||||||
|
export default prisma;
|
||||||
|
//# sourceMappingURL=prisma.js.map
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"prisma.js","sourceRoot":"","sources":["prisma.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,MAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,MAAM,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,YAAa,CAAC;AAEnD,MAAM,OAAO,GAAG,IAAI,QAAQ,CAAC,EAAE,gBAAgB,EAAE,CAAC,CAAC;AAEnD,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;AAE7C,eAAe,MAAM,CAAC"}
|
{"version":3,"file":"prisma.js","sourceRoot":"","sources":["prisma.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC1B,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,MAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,MAAM,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,YAAa,CAAC;AAEnD,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,EAAE,gBAAgB,EAAE,CAAC,CAAC;AAC5C,MAAM,OAAO,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC;AAEnC,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;AAE7C,eAAe,MAAM,CAAC"}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient } from "@prisma/client";
|
||||||
import { PrismaPg } from "@prisma/adapter-pg";
|
import { PrismaPg } from "@prisma/adapter-pg";
|
||||||
|
import { Pool } from "pg";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const connectionString = process.env.DATABASE_URL!;
|
const connectionString = process.env.DATABASE_URL!;
|
||||||
|
|
||||||
const adapter = new PrismaPg({ connectionString });
|
const pool = new Pool({ connectionString });
|
||||||
|
const adapter = new PrismaPg(pool);
|
||||||
|
|
||||||
const prisma = new PrismaClient({ adapter });
|
const prisma = new PrismaClient({ adapter });
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Elysia } from "elysia";
|
||||||
|
import { jwt } from "@elysiajs/jwt";
|
||||||
|
export const adminOnly = new Elysia({ name: "adminOnly" })
|
||||||
|
.use(jwt({
|
||||||
|
name: "jwt",
|
||||||
|
secret: process.env.JWT_SECRET || "fallback-secret"
|
||||||
|
}))
|
||||||
|
.onBeforeHandle(async ({ jwt, headers, set }) => {
|
||||||
|
const auth = headers.authorization;
|
||||||
|
if (!auth?.startsWith("Bearer ")) {
|
||||||
|
set.status = 401;
|
||||||
|
return { error: "Unauthorized: Missing or invalid token format" };
|
||||||
|
}
|
||||||
|
const token = auth.substring(7);
|
||||||
|
const payload = await jwt.verify(token);
|
||||||
|
if (!payload) {
|
||||||
|
set.status = 401;
|
||||||
|
return { error: "Unauthorized: Invalid token" };
|
||||||
|
}
|
||||||
|
if (payload.role !== "ADMIN") {
|
||||||
|
set.status = 401;
|
||||||
|
return { error: "Unauthorized: Admin access required" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
//# sourceMappingURL=auth.js.map
|
||||||
+37
@@ -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: {};
|
derive: {};
|
||||||
resolve: {};
|
resolve: {};
|
||||||
|
|||||||
@@ -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"}
|
||||||
@@ -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 @@
|
|||||||
{"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":"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 truncated
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Vendored
+38
-1
@@ -49,11 +49,11 @@ export declare const heroRoute: Elysia<"/hero", {
|
|||||||
post: {
|
post: {
|
||||||
body: {
|
body: {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
imageUrl: string;
|
||||||
title: string;
|
title: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
tagline: string;
|
tagline: string;
|
||||||
description: string;
|
description: string;
|
||||||
imageUrl: string;
|
|
||||||
buttonText: string;
|
buttonText: string;
|
||||||
buttonLink: 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: {};
|
derive: {};
|
||||||
resolve: {};
|
resolve: {};
|
||||||
|
|||||||
@@ -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"}
|
||||||
@@ -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 @@
|
|||||||
{"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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Vendored
+77
@@ -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
|
||||||
@@ -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"}
|
||||||
@@ -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
|
||||||
@@ -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"}
|
||||||
Vendored
+10
@@ -48,6 +48,16 @@ export declare const menusRoute: Elysia<"/menus", {
|
|||||||
put: any;
|
put: any;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
} & {
|
||||||
|
menus: {
|
||||||
|
items: {
|
||||||
|
":id": {
|
||||||
|
translations: {
|
||||||
|
patch: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
}, {
|
}, {
|
||||||
derive: {};
|
derive: {};
|
||||||
resolve: {};
|
resolve: {};
|
||||||
|
|||||||
@@ -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"}
|
||||||
@@ -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 @@
|
|||||||
{"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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Vendored
+8
@@ -107,6 +107,14 @@ export declare const pagesRoute: Elysia<"/pages", {
|
|||||||
delete: any;
|
delete: any;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
} & {
|
||||||
|
pages: {
|
||||||
|
":id": {
|
||||||
|
translations: {
|
||||||
|
patch: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
} & {
|
} & {
|
||||||
pages: {
|
pages: {
|
||||||
":slug": {
|
":slug": {
|
||||||
|
|||||||
@@ -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"}
|
||||||
@@ -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 @@
|
|||||||
{"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
|
||||||
@@ -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
@@ -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: {
|
products: {
|
||||||
get: {
|
get: {
|
||||||
|
|||||||
@@ -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"}
|
||||||
@@ -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 @@
|
|||||||
{"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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
@@ -24,6 +24,12 @@ export declare const settingsRoute: Elysia<"/settings", {
|
|||||||
settings: {
|
settings: {
|
||||||
put: any;
|
put: any;
|
||||||
};
|
};
|
||||||
|
} & {
|
||||||
|
settings: {
|
||||||
|
translations: {
|
||||||
|
patch: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
}, {
|
}, {
|
||||||
derive: {};
|
derive: {};
|
||||||
resolve: {};
|
resolve: {};
|
||||||
|
|||||||
@@ -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"}
|
||||||
@@ -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 @@
|
|||||||
{"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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Vendored
+2
@@ -29,10 +29,12 @@ export declare const uploadRoute: Elysia<"/upload", {
|
|||||||
success?: never;
|
success?: never;
|
||||||
url?: never;
|
url?: never;
|
||||||
filename?: never;
|
filename?: never;
|
||||||
|
path?: never;
|
||||||
} | {
|
} | {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
url: string;
|
url: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
|
path: string;
|
||||||
error?: never;
|
error?: never;
|
||||||
};
|
};
|
||||||
422: {
|
422: {
|
||||||
|
|||||||
@@ -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"}
|
||||||
@@ -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 @@
|
|||||||
{"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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"}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Elysia, t } from "elysia";
|
import { Elysia, t } from "elysia";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { mkdir } from "fs/promises";
|
import { mkdir } from "fs/promises";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
import prisma from "../lib/prisma";
|
import prisma from "../lib/prisma";
|
||||||
|
|
||||||
export const uploadRoute = new Elysia({ prefix: "/upload" })
|
export const uploadRoute = new Elysia({ prefix: "/upload" })
|
||||||
@@ -22,7 +23,7 @@ export const uploadRoute = new Elysia({ prefix: "/upload" })
|
|||||||
|
|
||||||
// Generate unique filename
|
// Generate unique filename
|
||||||
const ext = file.name.split(".").pop();
|
const ext = file.name.split(".").pop();
|
||||||
const filename = `${crypto.randomUUID()}.${ext}`;
|
const filename = `${randomUUID()}.${ext}`;
|
||||||
const uploadDir = join(process.cwd(), "public", "uploads");
|
const uploadDir = join(process.cwd(), "public", "uploads");
|
||||||
const filePath = join(uploadDir, filename);
|
const filePath = join(uploadDir, filename);
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"root":["./prisma.config.ts","./prisma/seed-pages.ts","./scripts/migrate-i18n.ts","./src/index.ts","./src/lib/lineClient.ts","./src/lib/notifications.ts","./src/lib/prisma.ts","./src/middleware/auth.ts","./src/routes/articles.ts","./src/routes/auth.ts","./src/routes/categories.ts","./src/routes/coupons.ts","./src/routes/customers.ts","./src/routes/hero.ts","./src/routes/lazadaOrder.ts","./src/routes/line.ts","./src/routes/menus.ts","./src/routes/newsletter.ts","./src/routes/orders.ts","./src/routes/pages.ts","./src/routes/payments.ts","./src/routes/products.ts","./src/routes/profile.ts","./src/routes/promotions.ts","./src/routes/raw-materials.ts","./src/routes/reviews.ts","./src/routes/settings.ts","./src/routes/shopee.ts","./src/routes/tiktok.ts","./src/routes/tiktokOrder.ts","./src/routes/upload.ts"],"errors":true,"version":"6.0.3"}
|
{"root":["./check_cat.ts","./prisma.config.ts","./prisma/seed-pages.ts","./scripts/migrate-i18n.ts","./scripts/migrate-pages.ts","./scripts/migrate-production.ts","./src/index.ts","./src/lib/lineClient.ts","./src/lib/notifications.ts","./src/lib/prisma.ts","./src/middleware/auth.ts","./src/routes/articles.ts","./src/routes/auth.ts","./src/routes/categories.ts","./src/routes/coupons.ts","./src/routes/customers.ts","./src/routes/hero.ts","./src/routes/lazadaOrder.ts","./src/routes/line.ts","./src/routes/media.ts","./src/routes/menus.ts","./src/routes/newsletter.ts","./src/routes/orders.ts","./src/routes/pages.ts","./src/routes/payments.ts","./src/routes/products.ts","./src/routes/profile.ts","./src/routes/promotions.ts","./src/routes/raw-materials.ts","./src/routes/reviews.ts","./src/routes/settings.ts","./src/routes/shopee.ts","./src/routes/tiktok.ts","./src/routes/tiktokOrder.ts","./src/routes/upload.ts"],"errors":true,"version":"6.0.3"}
|
||||||
Reference in new issue
Block a user